mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 07:46:54 +08:00
fix: resolve multiple bugs preventing trader creation (#1138)
* fix: resolve multiple bugs preventing trader creation Bug fixes: 1. Fix time.Time scanning error - SQLite stores datetime as TEXT, now parsing manually 2. Fix foreign key mismatch - traders table referenced exchanges(id) but exchanges uses composite primary key (id, user_id) 3. Add missing backtestManager field to Server struct 4. Add missing Shutdown method to Server struct 5. Fix NewFuturesTrader call - pass userId parameter 6. Fix UpdateExchange call - pass all required parameters 7. Add migrateTradersTable() to fix existing databases These issues prevented creating new traders with 500 errors. * fix(api): fix balance extraction field name mismatch Binance API returns 'availableBalance' (camelCase) but code was looking for 'available_balance' (snake_case). Now supports both formats. Also added 'totalWalletBalance' as fallback for total balance extraction. * fix(frontend): add missing ConfirmDialogProvider to App The delete trader button required ConfirmDialogProvider to be wrapped around the App component for the confirmation dialog to work. --------- Co-authored-by: NOFX Trader <nofx@local.dev>
This commit is contained in:
@@ -186,9 +186,7 @@ func (d *Database) createTables() error {
|
||||
use_oi_top BOOLEAN DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ai_model_id) REFERENCES ai_models(id),
|
||||
FOREIGN KEY (exchange_id) REFERENCES exchanges(id)
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)`,
|
||||
|
||||
// 用户表
|
||||
@@ -390,6 +388,12 @@ func (d *Database) createTables() error {
|
||||
log.Printf("⚠️ 迁移exchanges表失败: %v", err)
|
||||
}
|
||||
|
||||
// 修复traders表的外键约束问题
|
||||
err = d.migrateTradersTable()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 迁移traders表失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -613,6 +617,91 @@ func (d *Database) migrateExchangesTable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateTradersTable 迁移traders表,移除外键约束
|
||||
func (d *Database) migrateTradersTable() error {
|
||||
// 检查traders表是否存在外键约束(通过尝试创建一个测试记录来判断)
|
||||
// 如果表已经没有外键约束,则跳过迁移
|
||||
var tableSQL string
|
||||
err := d.db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='traders'`).Scan(&tableSQL)
|
||||
if err != nil {
|
||||
// 表不存在,无需迁移
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查是否包含 FOREIGN KEY (exchange_id) 或 FOREIGN KEY (ai_model_id)
|
||||
if !strings.Contains(tableSQL, "FOREIGN KEY (exchange_id)") && !strings.Contains(tableSQL, "FOREIGN KEY (ai_model_id)") {
|
||||
// 已经没有这些外键约束,无需迁移
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("🔄 开始迁移traders表,移除外键约束...")
|
||||
|
||||
// 创建新的traders表,不包含exchange_id和ai_model_id的外键约束
|
||||
_, err = d.db.Exec(`
|
||||
CREATE TABLE traders_new (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL DEFAULT 'default',
|
||||
name TEXT NOT NULL,
|
||||
ai_model_id TEXT NOT NULL,
|
||||
exchange_id TEXT NOT NULL,
|
||||
initial_balance REAL NOT NULL,
|
||||
scan_interval_minutes INTEGER DEFAULT 3,
|
||||
is_running BOOLEAN DEFAULT 0,
|
||||
btc_eth_leverage INTEGER DEFAULT 5,
|
||||
altcoin_leverage INTEGER DEFAULT 5,
|
||||
trading_symbols TEXT DEFAULT '',
|
||||
use_coin_pool BOOLEAN DEFAULT 0,
|
||||
use_oi_top BOOLEAN DEFAULT 0,
|
||||
custom_prompt TEXT DEFAULT '',
|
||||
override_base_prompt BOOLEAN DEFAULT 0,
|
||||
system_prompt_template TEXT DEFAULT 'default',
|
||||
is_cross_margin BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建新traders表失败: %w", err)
|
||||
}
|
||||
|
||||
// 复制数据到新表
|
||||
_, err = d.db.Exec(`
|
||||
INSERT INTO traders_new (id, user_id, name, ai_model_id, exchange_id, initial_balance,
|
||||
scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols,
|
||||
use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template,
|
||||
is_cross_margin, created_at, updated_at)
|
||||
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance,
|
||||
scan_interval_minutes, is_running,
|
||||
COALESCE(btc_eth_leverage, 5), COALESCE(altcoin_leverage, 5),
|
||||
COALESCE(trading_symbols, ''), COALESCE(use_coin_pool, 0), COALESCE(use_oi_top, 0),
|
||||
COALESCE(custom_prompt, ''), COALESCE(override_base_prompt, 0),
|
||||
COALESCE(system_prompt_template, 'default'), COALESCE(is_cross_margin, 1),
|
||||
created_at, updated_at
|
||||
FROM traders
|
||||
`)
|
||||
if err != nil {
|
||||
// 如果复制失败,删除新表
|
||||
d.db.Exec(`DROP TABLE traders_new`)
|
||||
return fmt.Errorf("复制traders数据失败: %w", err)
|
||||
}
|
||||
|
||||
// 删除旧表
|
||||
_, err = d.db.Exec(`DROP TABLE traders`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除旧traders表失败: %w", err)
|
||||
}
|
||||
|
||||
// 重命名新表
|
||||
_, err = d.db.Exec(`ALTER TABLE traders_new RENAME TO traders`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("重命名traders表失败: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ traders表迁移完成,已移除外键约束")
|
||||
return nil
|
||||
}
|
||||
|
||||
// User 用户配置
|
||||
type User struct {
|
||||
ID string `json:"id"`
|
||||
@@ -744,32 +833,38 @@ func (d *Database) EnsureAdminUser() error {
|
||||
// GetUserByEmail 通过邮箱获取用户
|
||||
func (d *Database) GetUserByEmail(email string) (*User, error) {
|
||||
var user User
|
||||
var createdAt, updatedAt string
|
||||
err := d.db.QueryRow(`
|
||||
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
|
||||
FROM users WHERE email = ?
|
||||
`, email).Scan(
|
||||
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
|
||||
&user.OTPVerified, &user.CreatedAt, &user.UpdatedAt,
|
||||
&user.OTPVerified, &createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
user.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserByID 通过ID获取用户
|
||||
func (d *Database) GetUserByID(userID string) (*User, error) {
|
||||
var user User
|
||||
var createdAt, updatedAt string
|
||||
err := d.db.QueryRow(`
|
||||
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
|
||||
FROM users WHERE id = ?
|
||||
`, userID).Scan(
|
||||
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
|
||||
&user.OTPVerified, &user.CreatedAt, &user.UpdatedAt,
|
||||
&user.OTPVerified, &createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
user.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
@@ -826,14 +921,18 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
||||
models := make([]*AIModelConfig, 0)
|
||||
for rows.Next() {
|
||||
var model AIModelConfig
|
||||
var createdAt, updatedAt string
|
||||
err := rows.Scan(
|
||||
&model.ID, &model.UserID, &model.Name, &model.Provider,
|
||||
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
|
||||
&model.CreatedAt, &model.UpdatedAt,
|
||||
&createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 解析时间字符串
|
||||
model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
// 解密API Key
|
||||
model.APIKey = d.decryptSensitiveData(model.APIKey)
|
||||
models = append(models, &model)
|
||||
@@ -861,6 +960,7 @@ func (d *Database) GetAIModel(userID, modelID string) (*AIModelConfig, error) {
|
||||
|
||||
for _, uid := range candidates {
|
||||
var model AIModelConfig
|
||||
var createdAt, updatedAt string
|
||||
err := d.db.QueryRow(`
|
||||
SELECT id, user_id, name, provider, enabled, api_key,
|
||||
COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at
|
||||
@@ -876,10 +976,13 @@ func (d *Database) GetAIModel(userID, modelID string) (*AIModelConfig, error) {
|
||||
&model.APIKey,
|
||||
&model.CustomAPIURL,
|
||||
&model.CustomModelName,
|
||||
&model.CreatedAt,
|
||||
&model.UpdatedAt,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
)
|
||||
if err == nil {
|
||||
// 解析时间字符串
|
||||
model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
// 解密API Key(与 GetAIModels 行为保持一致)
|
||||
model.APIKey = d.decryptSensitiveData(model.APIKey)
|
||||
return &model, nil
|
||||
@@ -912,6 +1015,7 @@ func (d *Database) GetDefaultAIModel(userID string) (*AIModelConfig, error) {
|
||||
|
||||
func (d *Database) firstEnabledAIModel(userID string) (*AIModelConfig, error) {
|
||||
var model AIModelConfig
|
||||
var createdAt, updatedAt string
|
||||
err := d.db.QueryRow(`
|
||||
SELECT id, user_id, name, provider, enabled, api_key,
|
||||
COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at
|
||||
@@ -928,12 +1032,15 @@ func (d *Database) firstEnabledAIModel(userID string) (*AIModelConfig, error) {
|
||||
&model.APIKey,
|
||||
&model.CustomAPIURL,
|
||||
&model.CustomModelName,
|
||||
&model.CreatedAt,
|
||||
&model.UpdatedAt,
|
||||
&createdAt,
|
||||
&updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 解析时间字符串
|
||||
model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
// 解密API Key,避免上层拿到加密串导致下游认证失败
|
||||
model.APIKey = d.decryptSensitiveData(model.APIKey)
|
||||
return &model, nil
|
||||
@@ -1033,6 +1140,7 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
|
||||
COALESCE(aster_private_key, '') as aster_private_key,
|
||||
COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr,
|
||||
COALESCE(lighter_private_key, '') as lighter_private_key,
|
||||
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
|
||||
created_at, updated_at
|
||||
FROM exchanges WHERE user_id = ? ORDER BY id
|
||||
`, userID)
|
||||
@@ -1045,23 +1153,30 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
|
||||
exchanges := make([]*ExchangeConfig, 0)
|
||||
for rows.Next() {
|
||||
var exchange ExchangeConfig
|
||||
var createdAt, updatedAt string
|
||||
err := rows.Scan(
|
||||
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type,
|
||||
&exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet,
|
||||
&exchange.HyperliquidWalletAddr, &exchange.AsterUser,
|
||||
&exchange.AsterSigner, &exchange.AsterPrivateKey,
|
||||
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey,
|
||||
&exchange.CreatedAt, &exchange.UpdatedAt,
|
||||
&exchange.LighterAPIKeyPrivateKey,
|
||||
&createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解析时间字符串
|
||||
exchange.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
exchange.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
|
||||
// 解密敏感字段
|
||||
exchange.APIKey = d.decryptSensitiveData(exchange.APIKey)
|
||||
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
|
||||
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
|
||||
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
|
||||
exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey)
|
||||
|
||||
exchanges = append(exchanges, &exchange)
|
||||
}
|
||||
@@ -1243,6 +1358,7 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
||||
var traders []*TraderRecord
|
||||
for rows.Next() {
|
||||
var trader TraderRecord
|
||||
var createdAt, updatedAt string
|
||||
err := rows.Scan(
|
||||
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
||||
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
|
||||
@@ -1250,11 +1366,14 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
||||
&trader.UseCoinPool, &trader.UseOITop,
|
||||
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
|
||||
&trader.IsCrossMargin,
|
||||
&trader.CreatedAt, &trader.UpdatedAt,
|
||||
&createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 解析时间字符串
|
||||
trader.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
trader.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
traders = append(traders, &trader)
|
||||
}
|
||||
|
||||
@@ -1307,6 +1426,9 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
|
||||
var trader TraderRecord
|
||||
var aiModel AIModelConfig
|
||||
var exchange ExchangeConfig
|
||||
var traderCreatedAt, traderUpdatedAt string
|
||||
var aiModelCreatedAt, aiModelUpdatedAt string
|
||||
var exchangeCreatedAt, exchangeUpdatedAt string
|
||||
|
||||
err := d.db.QueryRow(`
|
||||
SELECT
|
||||
@@ -1332,6 +1454,7 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
|
||||
COALESCE(e.aster_private_key, '') as aster_private_key,
|
||||
COALESCE(e.lighter_wallet_addr, '') as lighter_wallet_addr,
|
||||
COALESCE(e.lighter_private_key, '') as lighter_private_key,
|
||||
COALESCE(e.lighter_api_key_private_key, '') as lighter_api_key_private_key,
|
||||
e.created_at, e.updated_at
|
||||
FROM traders t
|
||||
JOIN ai_models a ON t.ai_model_id = a.id AND t.user_id = a.user_id
|
||||
@@ -1344,27 +1467,36 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
|
||||
&trader.UseCoinPool, &trader.UseOITop,
|
||||
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
|
||||
&trader.IsCrossMargin,
|
||||
&trader.CreatedAt, &trader.UpdatedAt,
|
||||
&traderCreatedAt, &traderUpdatedAt,
|
||||
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
|
||||
&aiModel.CustomAPIURL, &aiModel.CustomModelName,
|
||||
&aiModel.CreatedAt, &aiModel.UpdatedAt,
|
||||
&aiModelCreatedAt, &aiModelUpdatedAt,
|
||||
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled,
|
||||
&exchange.APIKey, &exchange.SecretKey, &exchange.Testnet,
|
||||
&exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey,
|
||||
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey,
|
||||
&exchange.CreatedAt, &exchange.UpdatedAt,
|
||||
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey, &exchange.LighterAPIKeyPrivateKey,
|
||||
&exchangeCreatedAt, &exchangeUpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
// 解析时间字符串
|
||||
trader.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", traderCreatedAt)
|
||||
trader.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", traderUpdatedAt)
|
||||
aiModel.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", aiModelCreatedAt)
|
||||
aiModel.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", aiModelUpdatedAt)
|
||||
exchange.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeCreatedAt)
|
||||
exchange.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeUpdatedAt)
|
||||
|
||||
// 解密敏感数据
|
||||
aiModel.APIKey = d.decryptSensitiveData(aiModel.APIKey)
|
||||
exchange.APIKey = d.decryptSensitiveData(exchange.APIKey)
|
||||
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
|
||||
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
|
||||
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
|
||||
exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey)
|
||||
|
||||
return &trader, &aiModel, &exchange, nil
|
||||
}
|
||||
@@ -1396,16 +1528,19 @@ func (d *Database) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string)
|
||||
// GetUserSignalSource 获取用户信号源配置
|
||||
func (d *Database) GetUserSignalSource(userID string) (*UserSignalSource, error) {
|
||||
var source UserSignalSource
|
||||
var createdAt, updatedAt string
|
||||
err := d.db.QueryRow(`
|
||||
SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at
|
||||
FROM user_signal_sources WHERE user_id = ?
|
||||
`, userID).Scan(
|
||||
&source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL,
|
||||
&source.CreatedAt, &source.UpdatedAt,
|
||||
&createdAt, &updatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
source.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
source.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
return &source, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user