fix: resolve multiple bugs preventing trader creation (#1138)

* fix: resolve multiple bugs preventing trader creation
Bug fixes:
1. Fix time.Time scanning error - SQLite stores datetime as TEXT, now parsing manually
2. Fix foreign key mismatch - traders table referenced exchanges(id) but exchanges uses composite primary key (id, user_id)
3. Add missing backtestManager field to Server struct
4. Add missing Shutdown method to Server struct
5. Fix NewFuturesTrader call - pass userId parameter
6. Fix UpdateExchange call - pass all required parameters
7. Add migrateTradersTable() to fix existing databases
These issues prevented creating new traders with 500 errors.
* fix(api): fix balance extraction field name mismatch
Binance API returns 'availableBalance' (camelCase) but code was looking for
'available_balance' (snake_case). Now supports both formats.
Also added 'totalWalletBalance' as fallback for total balance extraction.
* fix(frontend): add missing ConfirmDialogProvider to App
The delete trader button required ConfirmDialogProvider to be wrapped
around the App component for the confirmation dialog to work.
---------
Co-authored-by: NOFX Trader <nofx@local.dev>
This commit is contained in:
Professor-Chen
2025-11-30 12:22:20 +08:00
committed by GitHub
parent 11b6c6ba3e
commit c34a6c6bcf
3 changed files with 407 additions and 282 deletions

View File

@@ -186,9 +186,7 @@ func (d *Database) createTables() error {
use_oi_top BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (ai_model_id) REFERENCES ai_models(id),
FOREIGN KEY (exchange_id) REFERENCES exchanges(id)
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)`,
// 用户表
@@ -390,6 +388,12 @@ func (d *Database) createTables() error {
log.Printf("⚠️ 迁移exchanges表失败: %v", err)
}
// 修复traders表的外键约束问题
err = d.migrateTradersTable()
if err != nil {
log.Printf("⚠️ 迁移traders表失败: %v", err)
}
return nil
}
@@ -613,6 +617,91 @@ func (d *Database) migrateExchangesTable() error {
return nil
}
// migrateTradersTable 迁移traders表移除外键约束
func (d *Database) migrateTradersTable() error {
// 检查traders表是否存在外键约束通过尝试创建一个测试记录来判断
// 如果表已经没有外键约束,则跳过迁移
var tableSQL string
err := d.db.QueryRow(`SELECT sql FROM sqlite_master WHERE type='table' AND name='traders'`).Scan(&tableSQL)
if err != nil {
// 表不存在,无需迁移
return nil
}
// 检查是否包含 FOREIGN KEY (exchange_id) 或 FOREIGN KEY (ai_model_id)
if !strings.Contains(tableSQL, "FOREIGN KEY (exchange_id)") && !strings.Contains(tableSQL, "FOREIGN KEY (ai_model_id)") {
// 已经没有这些外键约束,无需迁移
return nil
}
log.Printf("🔄 开始迁移traders表移除外键约束...")
// 创建新的traders表不包含exchange_id和ai_model_id的外键约束
_, err = d.db.Exec(`
CREATE TABLE traders_new (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL DEFAULT 'default',
name TEXT NOT NULL,
ai_model_id TEXT NOT NULL,
exchange_id TEXT NOT NULL,
initial_balance REAL NOT NULL,
scan_interval_minutes INTEGER DEFAULT 3,
is_running BOOLEAN DEFAULT 0,
btc_eth_leverage INTEGER DEFAULT 5,
altcoin_leverage INTEGER DEFAULT 5,
trading_symbols TEXT DEFAULT '',
use_coin_pool BOOLEAN DEFAULT 0,
use_oi_top BOOLEAN DEFAULT 0,
custom_prompt TEXT DEFAULT '',
override_base_prompt BOOLEAN DEFAULT 0,
system_prompt_template TEXT DEFAULT 'default',
is_cross_margin BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
)
`)
if err != nil {
return fmt.Errorf("创建新traders表失败: %w", err)
}
// 复制数据到新表
_, err = d.db.Exec(`
INSERT INTO traders_new (id, user_id, name, ai_model_id, exchange_id, initial_balance,
scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols,
use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template,
is_cross_margin, created_at, updated_at)
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance,
scan_interval_minutes, is_running,
COALESCE(btc_eth_leverage, 5), COALESCE(altcoin_leverage, 5),
COALESCE(trading_symbols, ''), COALESCE(use_coin_pool, 0), COALESCE(use_oi_top, 0),
COALESCE(custom_prompt, ''), COALESCE(override_base_prompt, 0),
COALESCE(system_prompt_template, 'default'), COALESCE(is_cross_margin, 1),
created_at, updated_at
FROM traders
`)
if err != nil {
// 如果复制失败,删除新表
d.db.Exec(`DROP TABLE traders_new`)
return fmt.Errorf("复制traders数据失败: %w", err)
}
// 删除旧表
_, err = d.db.Exec(`DROP TABLE traders`)
if err != nil {
return fmt.Errorf("删除旧traders表失败: %w", err)
}
// 重命名新表
_, err = d.db.Exec(`ALTER TABLE traders_new RENAME TO traders`)
if err != nil {
return fmt.Errorf("重命名traders表失败: %w", err)
}
log.Printf("✅ traders表迁移完成已移除外键约束")
return nil
}
// User 用户配置
type User struct {
ID string `json:"id"`
@@ -744,32 +833,38 @@ func (d *Database) EnsureAdminUser() error {
// GetUserByEmail 通过邮箱获取用户
func (d *Database) GetUserByEmail(email string) (*User, error) {
var user User
var createdAt, updatedAt string
err := d.db.QueryRow(`
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
FROM users WHERE email = ?
`, email).Scan(
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
&user.OTPVerified, &user.CreatedAt, &user.UpdatedAt,
&user.OTPVerified, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
user.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
return &user, nil
}
// GetUserByID 通过ID获取用户
func (d *Database) GetUserByID(userID string) (*User, error) {
var user User
var createdAt, updatedAt string
err := d.db.QueryRow(`
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
FROM users WHERE id = ?
`, userID).Scan(
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
&user.OTPVerified, &user.CreatedAt, &user.UpdatedAt,
&user.OTPVerified, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
user.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
user.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
return &user, nil
}
@@ -826,14 +921,18 @@ func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
models := make([]*AIModelConfig, 0)
for rows.Next() {
var model AIModelConfig
var createdAt, updatedAt string
err := rows.Scan(
&model.ID, &model.UserID, &model.Name, &model.Provider,
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
&model.CreatedAt, &model.UpdatedAt,
&createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
// 解析时间字符串
model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
// 解密API Key
model.APIKey = d.decryptSensitiveData(model.APIKey)
models = append(models, &model)
@@ -861,6 +960,7 @@ func (d *Database) GetAIModel(userID, modelID string) (*AIModelConfig, error) {
for _, uid := range candidates {
var model AIModelConfig
var createdAt, updatedAt string
err := d.db.QueryRow(`
SELECT id, user_id, name, provider, enabled, api_key,
COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at
@@ -876,10 +976,13 @@ func (d *Database) GetAIModel(userID, modelID string) (*AIModelConfig, error) {
&model.APIKey,
&model.CustomAPIURL,
&model.CustomModelName,
&model.CreatedAt,
&model.UpdatedAt,
&createdAt,
&updatedAt,
)
if err == nil {
// 解析时间字符串
model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
// 解密API Key与 GetAIModels 行为保持一致)
model.APIKey = d.decryptSensitiveData(model.APIKey)
return &model, nil
@@ -912,6 +1015,7 @@ func (d *Database) GetDefaultAIModel(userID string) (*AIModelConfig, error) {
func (d *Database) firstEnabledAIModel(userID string) (*AIModelConfig, error) {
var model AIModelConfig
var createdAt, updatedAt string
err := d.db.QueryRow(`
SELECT id, user_id, name, provider, enabled, api_key,
COALESCE(custom_api_url, ''), COALESCE(custom_model_name, ''), created_at, updated_at
@@ -928,12 +1032,15 @@ func (d *Database) firstEnabledAIModel(userID string) (*AIModelConfig, error) {
&model.APIKey,
&model.CustomAPIURL,
&model.CustomModelName,
&model.CreatedAt,
&model.UpdatedAt,
&createdAt,
&updatedAt,
)
if err != nil {
return nil, err
}
// 解析时间字符串
model.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
model.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
// 解密API Key避免上层拿到加密串导致下游认证失败
model.APIKey = d.decryptSensitiveData(model.APIKey)
return &model, nil
@@ -1033,6 +1140,7 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
COALESCE(aster_private_key, '') as aster_private_key,
COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr,
COALESCE(lighter_private_key, '') as lighter_private_key,
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key,
created_at, updated_at
FROM exchanges WHERE user_id = ? ORDER BY id
`, userID)
@@ -1045,23 +1153,30 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
exchanges := make([]*ExchangeConfig, 0)
for rows.Next() {
var exchange ExchangeConfig
var createdAt, updatedAt string
err := rows.Scan(
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type,
&exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet,
&exchange.HyperliquidWalletAddr, &exchange.AsterUser,
&exchange.AsterSigner, &exchange.AsterPrivateKey,
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey,
&exchange.CreatedAt, &exchange.UpdatedAt,
&exchange.LighterAPIKeyPrivateKey,
&createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
// 解析时间字符串
exchange.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
exchange.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
// 解密敏感字段
exchange.APIKey = d.decryptSensitiveData(exchange.APIKey)
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey)
exchanges = append(exchanges, &exchange)
}
@@ -1243,6 +1358,7 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
var traders []*TraderRecord
for rows.Next() {
var trader TraderRecord
var createdAt, updatedAt string
err := rows.Scan(
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
@@ -1250,11 +1366,14 @@ func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
&trader.UseCoinPool, &trader.UseOITop,
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
&trader.IsCrossMargin,
&trader.CreatedAt, &trader.UpdatedAt,
&createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
// 解析时间字符串
trader.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
trader.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
traders = append(traders, &trader)
}
@@ -1307,6 +1426,9 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
var trader TraderRecord
var aiModel AIModelConfig
var exchange ExchangeConfig
var traderCreatedAt, traderUpdatedAt string
var aiModelCreatedAt, aiModelUpdatedAt string
var exchangeCreatedAt, exchangeUpdatedAt string
err := d.db.QueryRow(`
SELECT
@@ -1332,6 +1454,7 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
COALESCE(e.aster_private_key, '') as aster_private_key,
COALESCE(e.lighter_wallet_addr, '') as lighter_wallet_addr,
COALESCE(e.lighter_private_key, '') as lighter_private_key,
COALESCE(e.lighter_api_key_private_key, '') as lighter_api_key_private_key,
e.created_at, e.updated_at
FROM traders t
JOIN ai_models a ON t.ai_model_id = a.id AND t.user_id = a.user_id
@@ -1344,27 +1467,36 @@ func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIM
&trader.UseCoinPool, &trader.UseOITop,
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
&trader.IsCrossMargin,
&trader.CreatedAt, &trader.UpdatedAt,
&traderCreatedAt, &traderUpdatedAt,
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
&aiModel.CustomAPIURL, &aiModel.CustomModelName,
&aiModel.CreatedAt, &aiModel.UpdatedAt,
&aiModelCreatedAt, &aiModelUpdatedAt,
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled,
&exchange.APIKey, &exchange.SecretKey, &exchange.Testnet,
&exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey,
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey,
&exchange.CreatedAt, &exchange.UpdatedAt,
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey, &exchange.LighterAPIKeyPrivateKey,
&exchangeCreatedAt, &exchangeUpdatedAt,
)
if err != nil {
return nil, nil, nil, err
}
// 解析时间字符串
trader.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", traderCreatedAt)
trader.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", traderUpdatedAt)
aiModel.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", aiModelCreatedAt)
aiModel.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", aiModelUpdatedAt)
exchange.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeCreatedAt)
exchange.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", exchangeUpdatedAt)
// 解密敏感数据
aiModel.APIKey = d.decryptSensitiveData(aiModel.APIKey)
exchange.APIKey = d.decryptSensitiveData(exchange.APIKey)
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
exchange.LighterAPIKeyPrivateKey = d.decryptSensitiveData(exchange.LighterAPIKeyPrivateKey)
return &trader, &aiModel, &exchange, nil
}
@@ -1396,16 +1528,19 @@ func (d *Database) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string)
// GetUserSignalSource 获取用户信号源配置
func (d *Database) GetUserSignalSource(userID string) (*UserSignalSource, error) {
var source UserSignalSource
var createdAt, updatedAt string
err := d.db.QueryRow(`
SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at
FROM user_signal_sources WHERE user_id = ?
`, userID).Scan(
&source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL,
&source.CreatedAt, &source.UpdatedAt,
&createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
source.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
source.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
return &source, nil
}