mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
befc887a89
* fix(ci): add test encryption key for CI environment - Add DATA_ENCRYPTION_KEY environment variable to PR test workflow - Add test RSA public key for encryption tests in CI - Ensures unit tests pass in CI without production credentials Co-authored-by: tinkle-community <tinklefund@gmail.com> * fix(ci): install Go cover tool to eliminate covdata warnings - Add step to install golang.org/x/tools/cmd/cover in CI workflow - Use || true to prevent installation failure from breaking CI - Eliminates "no such tool covdata" warnings during test execution - Apply go fmt to multiple files for consistency Co-authored-by: tinkle-community <tinklefund@gmail.com> * fix(ci): install covdata tool for Go 1.23 coverage The CI was failing with "go: no such tool 'covdata'" error. This is because Go 1.23 requires the covdata tool to be installed for coverage reporting. Changes: - Install golang.org/x/tools/cmd/covdata in CI workflow - Update step name to reflect both coverage tools being installed Fixes the unit test failures in CI pipeline. Co-Authored-By: tinkle-community <tinklefund@gmail.com> * fix(ci): remove unnecessary covdata installation and use builtin go tool cover The previous attempt to install golang.org/x/tools/cmd/covdata was failing because the package structure changed in Go 1.23 and tools v0.38.0. The covdata tool is not needed for this project since we only use simple coverage reporting with go test -coverprofile. The go tool cover command is built into the Go toolchain and requires no additional installation. Changes: - Remove failed covdata and cover installation attempts - Add verification step for go tool cover availability - Simplify CI pipeline by eliminating unnecessary dependencies Co-authored-by: tinkle-community <tinklefund@gmail.com> * fix(ci): upgrade Go version to 1.25 to match go.mod declaration The CI was using Go 1.23 while go.mod declares go 1.25.0, causing "no such tool covdata" errors during coverage test compilation. Go 1.25's coverage infrastructure requires toolchain features not available in Go 1.23. This change aligns the CI Go version with the project's declared version requirement, ensuring the full Go 1.25 toolchain (including the covdata tool) is available for coverage testing. Co-authored-by: tinkle-community <tinklefund@gmail.com> --------- Co-authored-by: tinkle-community <tinklefund@gmail.com>
1274 lines
45 KiB
Go
1274 lines
45 KiB
Go
package config
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"database/sql"
|
||
"encoding/base32"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"nofx/crypto"
|
||
"nofx/market"
|
||
"os"
|
||
"slices"
|
||
"strings"
|
||
"time"
|
||
|
||
_ "modernc.org/sqlite"
|
||
)
|
||
|
||
// DatabaseInterface 定义了数据库实现需要提供的方法集合
|
||
type DatabaseInterface interface {
|
||
SetCryptoService(cs *crypto.CryptoService)
|
||
CreateUser(user *User) error
|
||
GetUserByEmail(email string) (*User, error)
|
||
GetUserByID(userID string) (*User, error)
|
||
GetAllUsers() ([]string, error)
|
||
UpdateUserOTPVerified(userID string, verified bool) error
|
||
GetAIModels(userID string) ([]*AIModelConfig, error)
|
||
UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error
|
||
GetExchanges(userID string) ([]*ExchangeConfig, error)
|
||
UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error
|
||
CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error
|
||
CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error
|
||
CreateTrader(trader *TraderRecord) error
|
||
GetTraders(userID string) ([]*TraderRecord, error)
|
||
UpdateTraderStatus(userID, id string, isRunning bool) error
|
||
UpdateTrader(trader *TraderRecord) error
|
||
UpdateTraderInitialBalance(userID, id string, newBalance float64) error
|
||
UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error
|
||
DeleteTrader(userID, id string) error
|
||
GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error)
|
||
GetSystemConfig(key string) (string, error)
|
||
SetSystemConfig(key, value string) error
|
||
CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error
|
||
GetUserSignalSource(userID string) (*UserSignalSource, error)
|
||
UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error
|
||
GetCustomCoins() []string
|
||
LoadBetaCodesFromFile(filePath string) error
|
||
ValidateBetaCode(code string) (bool, error)
|
||
UseBetaCode(code, userEmail string) error
|
||
GetBetaCodeStats() (total, used int, err error)
|
||
Close() error
|
||
}
|
||
|
||
// Database 配置数据库
|
||
type Database struct {
|
||
db *sql.DB
|
||
cryptoService *crypto.CryptoService
|
||
}
|
||
|
||
// NewDatabase 创建配置数据库
|
||
func NewDatabase(dbPath string) (*Database, error) {
|
||
db, err := sql.Open("sqlite", dbPath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("打开数据库失败: %w", err)
|
||
}
|
||
|
||
// 🔒 启用 WAL 模式,提高并发性能和崩溃恢复能力
|
||
// WAL (Write-Ahead Logging) 模式的优势:
|
||
// 1. 更好的并发性能:读操作不会被写操作阻塞
|
||
// 2. 崩溃安全:即使在断电或强制终止时也能保证数据完整性
|
||
// 3. 更快的写入:不需要每次都写入主数据库文件
|
||
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||
db.Close()
|
||
return nil, fmt.Errorf("启用WAL模式失败: %w", err)
|
||
}
|
||
|
||
// 🔒 设置 synchronous=FULL 确保数据持久性
|
||
// FULL (2) 模式: 确保数据在关键时刻完全写入磁盘
|
||
// 配合 WAL 模式,在保证数据安全的同时获得良好性能
|
||
if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil {
|
||
db.Close()
|
||
return nil, fmt.Errorf("设置synchronous失败: %w", err)
|
||
}
|
||
|
||
database := &Database{db: db}
|
||
if err := database.createTables(); err != nil {
|
||
return nil, fmt.Errorf("创建表失败: %w", err)
|
||
}
|
||
|
||
if err := database.initDefaultData(); err != nil {
|
||
return nil, fmt.Errorf("初始化默认数据失败: %w", err)
|
||
}
|
||
|
||
log.Printf("✅ 数据库已启用 WAL 模式和 FULL 同步,数据持久性得到保证")
|
||
return database, nil
|
||
}
|
||
|
||
// createTables 创建数据库表
|
||
func (d *Database) createTables() error {
|
||
queries := []string{
|
||
// AI模型配置表
|
||
`CREATE TABLE IF NOT EXISTS ai_models (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT NOT NULL,
|
||
provider TEXT NOT NULL,
|
||
enabled BOOLEAN DEFAULT 0,
|
||
api_key TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||
)`,
|
||
|
||
// 交易所配置表
|
||
`CREATE TABLE IF NOT EXISTS exchanges (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT NOT NULL,
|
||
type TEXT NOT NULL, -- 'cex' or 'dex'
|
||
enabled BOOLEAN DEFAULT 0,
|
||
api_key TEXT DEFAULT '',
|
||
secret_key TEXT DEFAULT '',
|
||
testnet BOOLEAN DEFAULT 0,
|
||
-- Hyperliquid 特定字段
|
||
hyperliquid_wallet_addr TEXT DEFAULT '',
|
||
-- Aster 特定字段
|
||
aster_user TEXT DEFAULT '',
|
||
aster_signer TEXT DEFAULT '',
|
||
aster_private_key TEXT DEFAULT '',
|
||
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{
|
||
"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"`
|
||
Email string `json:"email"`
|
||
PasswordHash string `json:"-"` // 不返回到前端
|
||
OTPSecret string `json:"-"` // 不返回到前端
|
||
OTPVerified bool `json:"otp_verified"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// AIModelConfig AI模型配置
|
||
type AIModelConfig struct {
|
||
ID string `json:"id"`
|
||
UserID string `json:"user_id"`
|
||
Name string `json:"name"`
|
||
Provider string `json:"provider"`
|
||
Enabled bool `json:"enabled"`
|
||
APIKey string `json:"apiKey"`
|
||
CustomAPIURL string `json:"customApiUrl"`
|
||
CustomModelName string `json:"customModelName"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// ExchangeConfig 交易所配置
|
||
type ExchangeConfig struct {
|
||
ID string `json:"id"`
|
||
UserID string `json:"user_id"`
|
||
Name string `json:"name"`
|
||
Type string `json:"type"`
|
||
Enabled bool `json:"enabled"`
|
||
APIKey string `json:"apiKey"` // For Binance: API Key; For Hyperliquid: Agent Private Key (should have ~0 balance)
|
||
SecretKey string `json:"secretKey"` // For Binance: Secret Key; Not used for Hyperliquid
|
||
Testnet bool `json:"testnet"`
|
||
// Hyperliquid Agent Wallet configuration (following official best practices)
|
||
// Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets
|
||
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Main Wallet Address (holds funds, never expose private key)
|
||
// Aster 特定字段
|
||
AsterUser string `json:"asterUser"`
|
||
AsterSigner string `json:"asterSigner"`
|
||
AsterPrivateKey string `json:"asterPrivateKey"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// TraderRecord 交易员配置(数据库实体)
|
||
type TraderRecord struct {
|
||
ID string `json:"id"`
|
||
UserID string `json:"user_id"`
|
||
Name string `json:"name"`
|
||
AIModelID string `json:"ai_model_id"`
|
||
ExchangeID string `json:"exchange_id"`
|
||
InitialBalance float64 `json:"initial_balance"`
|
||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
||
IsRunning bool `json:"is_running"`
|
||
BTCETHLeverage int `json:"btc_eth_leverage"` // BTC/ETH杠杆倍数
|
||
AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币杠杆倍数
|
||
TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔
|
||
UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源
|
||
UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源
|
||
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
|
||
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
|
||
SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称
|
||
IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式(true=全仓,false=逐仓)
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// UserSignalSource 用户信号源配置
|
||
type UserSignalSource struct {
|
||
ID int `json:"id"`
|
||
UserID string `json:"user_id"`
|
||
CoinPoolURL string `json:"coin_pool_url"`
|
||
OITopURL string `json:"oi_top_url"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// GenerateOTPSecret 生成OTP密钥
|
||
func GenerateOTPSecret() (string, error) {
|
||
secret := make([]byte, 20)
|
||
_, err := rand.Read(secret)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return base32.StdEncoding.EncodeToString(secret), nil
|
||
}
|
||
|
||
// CreateUser 创建用户
|
||
func (d *Database) CreateUser(user *User) error {
|
||
_, err := d.db.Exec(`
|
||
INSERT INTO users (id, email, password_hash, otp_secret, otp_verified)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, user.ID, user.Email, user.PasswordHash, user.OTPSecret, user.OTPVerified)
|
||
return err
|
||
}
|
||
|
||
// EnsureAdminUser 确保admin用户存在(用于管理员模式)
|
||
func (d *Database) EnsureAdminUser() error {
|
||
// 检查admin用户是否已存在
|
||
var count int
|
||
err := d.db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = 'admin'`).Scan(&count)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 如果已存在,直接返回
|
||
if count > 0 {
|
||
return nil
|
||
}
|
||
|
||
// 创建admin用户(密码为空,因为管理员模式下不需要密码)
|
||
adminUser := &User{
|
||
ID: "admin",
|
||
Email: "admin@localhost",
|
||
PasswordHash: "", // 管理员模式下不使用密码
|
||
OTPSecret: "",
|
||
OTPVerified: true,
|
||
}
|
||
|
||
return d.CreateUser(adminUser)
|
||
}
|
||
|
||
// GetUserByEmail 通过邮箱获取用户
|
||
func (d *Database) GetUserByEmail(email string) (*User, error) {
|
||
var user User
|
||
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
|
||
}
|
||
|
||
// UpdateUserPassword 更新用户密码
|
||
func (d *Database) UpdateUserPassword(userID, passwordHash string) error {
|
||
_, err := d.db.Exec(`
|
||
UPDATE users
|
||
SET password_hash = ?, updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?
|
||
`, passwordHash, userID)
|
||
return err
|
||
}
|
||
|
||
// GetAIModels 获取用户的AI模型配置
|
||
func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
||
rows, err := d.db.Query(`
|
||
SELECT id, user_id, name, provider, enabled, api_key,
|
||
COALESCE(custom_api_url, '') as custom_api_url,
|
||
COALESCE(custom_model_name, '') as custom_model_name,
|
||
created_at, updated_at
|
||
FROM ai_models WHERE user_id = ? ORDER BY id
|
||
`, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
// 初始化为空切片而不是nil,确保JSON序列化为[]而不是null
|
||
models := make([]*AIModelConfig, 0)
|
||
for rows.Next() {
|
||
var model AIModelConfig
|
||
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
|
||
}
|
||
// 解密API Key
|
||
model.APIKey = d.decryptSensitiveData(model.APIKey)
|
||
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),更新它
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
_, err = d.db.Exec(`
|
||
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
||
WHERE id = ? AND user_id = ?
|
||
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
|
||
return err
|
||
}
|
||
|
||
// ID 不存在,尝试兼容旧逻辑:将 id 作为 provider 查找
|
||
provider := id
|
||
err = d.db.QueryRow(`
|
||
SELECT id FROM ai_models WHERE user_id = ? AND provider = ? LIMIT 1
|
||
`, userID, provider).Scan(&existingID)
|
||
|
||
if err == nil {
|
||
// 找到了现有配置(通过 provider 匹配,兼容旧版),更新它
|
||
log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID)
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
_, err = d.db.Exec(`
|
||
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
||
WHERE id = ? AND user_id = ?
|
||
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
|
||
return err
|
||
}
|
||
|
||
// 没有找到任何现有配置,创建新的
|
||
// 推断 provider(从 id 中提取,或者直接使用 id)
|
||
if provider == id && (provider == "deepseek" || provider == "qwen") {
|
||
// id 本身就是 provider
|
||
provider = id
|
||
} else {
|
||
// 从 id 中提取 provider(假设格式是 userID_provider 或 timestamp_userID_provider)
|
||
parts := strings.Split(id, "_")
|
||
if len(parts) >= 2 {
|
||
provider = parts[len(parts)-1] // 取最后一部分作为 provider
|
||
} else {
|
||
provider = id
|
||
}
|
||
}
|
||
|
||
// 获取模型的基本信息
|
||
var name string
|
||
err = d.db.QueryRow(`
|
||
SELECT name FROM ai_models WHERE provider = ? LIMIT 1
|
||
`, provider).Scan(&name)
|
||
if err != nil {
|
||
// 如果找不到基本信息,使用默认值
|
||
if provider == "deepseek" {
|
||
name = "DeepSeek AI"
|
||
} else if provider == "qwen" {
|
||
name = "Qwen AI"
|
||
} else {
|
||
name = provider + " AI"
|
||
}
|
||
}
|
||
|
||
// 如果传入的 ID 已经是完整格式(如 "admin_deepseek_custom1"),直接使用
|
||
// 否则生成新的 ID
|
||
newModelID := id
|
||
if id == provider {
|
||
// id 就是 provider,生成新的用户特定 ID
|
||
newModelID = fmt.Sprintf("%s_%s", userID, provider)
|
||
}
|
||
|
||
log.Printf("✓ 创建新的 AI 模型配置: ID=%s, Provider=%s, Name=%s", newModelID, provider, name)
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
_, err = d.db.Exec(`
|
||
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||
`, newModelID, userID, name, provider, enabled, encryptedAPIKey, customAPIURL, customModelName)
|
||
|
||
return err
|
||
}
|
||
|
||
// GetExchanges 获取用户的交易所配置
|
||
func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
|
||
rows, err := d.db.Query(`
|
||
SELECT id, user_id, name, type, enabled, api_key, secret_key, testnet,
|
||
COALESCE(hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
|
||
COALESCE(aster_user, '') as aster_user,
|
||
COALESCE(aster_signer, '') as aster_signer,
|
||
COALESCE(aster_private_key, '') as aster_private_key,
|
||
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
|
||
}
|
||
|
||
// 解密敏感字段
|
||
exchange.APIKey = d.decryptSensitiveData(exchange.APIKey)
|
||
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
|
||
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
|
||
|
||
exchanges = append(exchanges, &exchange)
|
||
}
|
||
|
||
return exchanges, nil
|
||
}
|
||
|
||
// UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置
|
||
// 🔒 安全特性:空值不会覆盖现有的敏感字段(api_key, secret_key, aster_private_key)
|
||
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)
|
||
|
||
// 构建动态 UPDATE SET 子句
|
||
// 基础字段:总是更新
|
||
setClauses := []string{
|
||
"enabled = ?",
|
||
"testnet = ?",
|
||
"hyperliquid_wallet_addr = ?",
|
||
"aster_user = ?",
|
||
"aster_signer = ?",
|
||
"updated_at = datetime('now')",
|
||
}
|
||
args := []interface{}{enabled, testnet, hyperliquidWalletAddr, asterUser, asterSigner}
|
||
|
||
// 🔒 敏感字段:只在非空时更新(保护现有数据)
|
||
if apiKey != "" {
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
setClauses = append(setClauses, "api_key = ?")
|
||
args = append(args, encryptedAPIKey)
|
||
}
|
||
|
||
if secretKey != "" {
|
||
encryptedSecretKey := d.encryptSensitiveData(secretKey)
|
||
setClauses = append(setClauses, "secret_key = ?")
|
||
args = append(args, encryptedSecretKey)
|
||
}
|
||
|
||
if asterPrivateKey != "" {
|
||
encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey)
|
||
setClauses = append(setClauses, "aster_private_key = ?")
|
||
args = append(args, encryptedAsterPrivateKey)
|
||
}
|
||
|
||
// WHERE 条件
|
||
args = append(args, id, userID)
|
||
|
||
// 构建完整的 UPDATE 语句
|
||
query := fmt.Sprintf(`
|
||
UPDATE exchanges SET %s
|
||
WHERE id = ? AND user_id = ?
|
||
`, strings.Join(setClauses, ", "))
|
||
|
||
// 执行更新
|
||
result, err := d.db.Exec(query, args...)
|
||
if err != nil {
|
||
log.Printf("❌ UpdateExchange: 更新失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
// 检查是否有行被更新
|
||
rowsAffected, err := result.RowsAffected()
|
||
if err != nil {
|
||
log.Printf("❌ UpdateExchange: 获取影响行数失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
log.Printf("📊 UpdateExchange: 影响行数 = %d", rowsAffected)
|
||
|
||
// 如果没有行被更新,说明用户没有这个交易所的配置,需要创建
|
||
if rowsAffected == 0 {
|
||
log.Printf("💡 UpdateExchange: 没有现有记录,创建新记录")
|
||
|
||
// 根据交易所ID确定基本信息
|
||
var name, typ string
|
||
if id == "binance" {
|
||
name = "Binance Futures"
|
||
typ = "cex"
|
||
} else if id == "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 {
|
||
// 加密敏感字段
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
encryptedSecretKey := d.encryptSensitiveData(secretKey)
|
||
encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey)
|
||
|
||
_, err := d.db.Exec(`
|
||
INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`, id, userID, name, typ, enabled, encryptedAPIKey, encryptedSecretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, encryptedAsterPrivateKey)
|
||
return err
|
||
}
|
||
|
||
// CreateTrader 创建交易员
|
||
func (d *Database) CreateTrader(trader *TraderRecord) error {
|
||
_, err := d.db.Exec(`
|
||
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin)
|
||
return err
|
||
}
|
||
|
||
// GetTraders 获取用户的交易员
|
||
func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
||
rows, err := d.db.Query(`
|
||
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
|
||
COALESCE(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage,
|
||
COALESCE(trading_symbols, '') as trading_symbols,
|
||
COALESCE(use_coin_pool, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top,
|
||
COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt,
|
||
COALESCE(system_prompt_template, 'default') as system_prompt_template,
|
||
COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at
|
||
FROM traders WHERE user_id = ? ORDER BY created_at DESC
|
||
`, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var traders []*TraderRecord
|
||
for rows.Next() {
|
||
var trader TraderRecord
|
||
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,
|
||
COALESCE(t.btc_eth_leverage, 5) as btc_eth_leverage,
|
||
COALESCE(t.altcoin_leverage, 5) as altcoin_leverage,
|
||
COALESCE(t.trading_symbols, '') as trading_symbols,
|
||
COALESCE(t.use_coin_pool, 0) as use_coin_pool,
|
||
COALESCE(t.use_oi_top, 0) as use_oi_top,
|
||
COALESCE(t.custom_prompt, '') as custom_prompt,
|
||
COALESCE(t.override_base_prompt, 0) as override_base_prompt,
|
||
COALESCE(t.system_prompt_template, 'default') as system_prompt_template,
|
||
COALESCE(t.is_cross_margin, 1) as is_cross_margin,
|
||
t.created_at, t.updated_at,
|
||
a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key,
|
||
COALESCE(a.custom_api_url, '') as custom_api_url,
|
||
COALESCE(a.custom_model_name, '') as custom_model_name,
|
||
a.created_at, a.updated_at,
|
||
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet,
|
||
COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
|
||
COALESCE(e.aster_user, '') as aster_user,
|
||
COALESCE(e.aster_signer, '') as aster_signer,
|
||
COALESCE(e.aster_private_key, '') as aster_private_key,
|
||
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
|
||
}
|
||
|
||
// 解密敏感数据
|
||
aiModel.APIKey = d.decryptSensitiveData(aiModel.APIKey)
|
||
exchange.APIKey = d.decryptSensitiveData(exchange.APIKey)
|
||
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
|
||
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
|
||
|
||
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
|
||
}
|
||
|
||
// SetCryptoService 设置加密服务
|
||
func (d *Database) SetCryptoService(cs *crypto.CryptoService) {
|
||
d.cryptoService = cs
|
||
}
|
||
|
||
// encryptSensitiveData 加密敏感数据用于存储
|
||
func (d *Database) encryptSensitiveData(plaintext string) string {
|
||
if d.cryptoService == nil || plaintext == "" {
|
||
return plaintext
|
||
}
|
||
|
||
encrypted, err := d.cryptoService.EncryptForStorage(plaintext)
|
||
if err != nil {
|
||
log.Printf("⚠️ 加密失败: %v", err)
|
||
return plaintext // 返回明文作为降级处理
|
||
}
|
||
|
||
return encrypted
|
||
}
|
||
|
||
// decryptSensitiveData 解密敏感数据
|
||
func (d *Database) decryptSensitiveData(encrypted string) string {
|
||
if d.cryptoService == nil || encrypted == "" {
|
||
return encrypted
|
||
}
|
||
|
||
// 如果不是加密格式,直接返回
|
||
if !d.cryptoService.IsEncryptedStorageValue(encrypted) {
|
||
return encrypted
|
||
}
|
||
|
||
decrypted, err := d.cryptoService.DecryptFromStorage(encrypted)
|
||
if err != nil {
|
||
log.Printf("⚠️ 解密失败: %v", err)
|
||
return encrypted // 返回加密文本作为降级处理
|
||
}
|
||
|
||
return decrypted
|
||
}
|