Merge pull request #439 from NoFxAiOS/revert-411-beta

Revert "Beta feat: migrate from SQLite to PostgreSQL + Redis architecture"
This commit is contained in:
Icyoung
2025-11-04 21:48:22 +08:00
committed by GitHub
14 changed files with 12 additions and 1507 deletions
-12
View File
@@ -1,18 +1,6 @@
# NOFX Environment Variables Template
# Copy this file to .env and modify the values as needed
# PostgreSQL数据库配置
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DB=nofx
POSTGRES_USER=nofx
POSTGRES_PASSWORD=nofx123456
# Redis配置
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=redis123456
# Ports Configuration
# Backend API server port (internal: 8080, external: configurable)
NOFX_BACKEND_PORT=8080
+2 -2
View File
@@ -21,12 +21,12 @@ import (
type Server struct {
router *gin.Engine
traderManager *manager.TraderManager
database config.DatabaseInterface
database *config.Database
port int
}
// NewServer 创建API服务器
func NewServer(traderManager *manager.TraderManager, database config.DatabaseInterface, port int) *Server {
func NewServer(traderManager *manager.TraderManager, database *config.Database, port int) *Server {
// 设置为Release模式(减少日志输出)
gin.SetMode(gin.ReleaseMode)
+1 -47
View File
@@ -13,7 +13,6 @@ import (
"strings"
"time"
_ "github.com/lib/pq"
_ "github.com/mattn/go-sqlite3"
)
@@ -22,53 +21,8 @@ type Database struct {
db *sql.DB
}
// DatabaseInterface 数据库接口
type DatabaseInterface interface {
CreateUser(user *User) error
EnsureAdminUser() 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
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
}
// 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(兼容模式)
func NewDatabase(dbPath string) (*Database, error) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("打开数据库失败: %w", err)
-701
View File
@@ -1,701 +0,0 @@
package config
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"nofx/market"
"os"
"slices"
"strings"
"time"
_ "github.com/lib/pq"
)
// PostgreSQLDatabase PostgreSQL数据库配置
type PostgreSQLDatabase struct {
db *sql.DB
}
// NewPostgreSQLDatabase 创建PostgreSQL数据库连接
func NewPostgreSQLDatabase() (*PostgreSQLDatabase, error) {
// 从环境变量获取数据库连接信息
host := getEnv("POSTGRES_HOST", "localhost")
port := getEnv("POSTGRES_PORT", "5432")
dbname := getEnv("POSTGRES_DB", "nofx")
user := getEnv("POSTGRES_USER", "nofx")
password := getEnv("POSTGRES_PASSWORD", "nofx123456")
// 构建连接字符串
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
log.Printf("📋 连接PostgreSQL数据库: %s:%s/%s", host, port, dbname)
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("打开PostgreSQL数据库失败: %w", err)
}
// 测试连接
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("连接PostgreSQL数据库失败: %w", err)
}
// 设置连接池参数
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(time.Hour)
database := &PostgreSQLDatabase{db: db}
log.Printf("✅ PostgreSQL数据库连接成功")
return database, nil
}
// getEnv 获取环境变量,如果不存在返回默认值
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
// CreateUser 创建用户
func (d *PostgreSQLDatabase) CreateUser(user *User) error {
_, err := d.db.Exec(`
INSERT INTO users (id, email, password_hash, otp_secret, otp_verified)
VALUES ($1, $2, $3, $4, $5)
`, user.ID, user.Email, user.PasswordHash, user.OTPSecret, user.OTPVerified)
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
err := d.db.QueryRow(`
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
FROM users WHERE email = $1
`, 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 *PostgreSQLDatabase) 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 = $1
`, 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 *PostgreSQLDatabase) 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 *PostgreSQLDatabase) UpdateUserOTPVerified(userID string, verified bool) error {
_, err := d.db.Exec(`UPDATE users SET otp_verified = $1 WHERE id = $2`, verified, userID)
return err
}
// GetAIModels 获取用户的AI模型配置
func (d *PostgreSQLDatabase) 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 = $1 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 *PostgreSQLDatabase) 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 = $1 AND id = $2 LIMIT 1
`, userID, id).Scan(&existingID)
if err == nil {
// 找到了现有配置(精确匹配 ID),更新它
_, err = d.db.Exec(`
UPDATE ai_models SET enabled = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, updated_at = CURRENT_TIMESTAMP
WHERE id = $5 AND user_id = $6
`, 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 = $1 AND provider = $2 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 = $1, api_key = $2, custom_api_url = $3, custom_model_name = $4, updated_at = CURRENT_TIMESTAMP
WHERE id = $5 AND user_id = $6
`, 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 = $1 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 ($1, $2, $3, $4, $5, $6, $7, $8, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, newModelID, userID, name, provider, enabled, apiKey, customAPIURL, customModelName)
return err
}
// GetExchanges 获取用户的交易所配置
func (d *PostgreSQLDatabase) 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 = $1 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 *PostgreSQLDatabase) 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 = $1, api_key = $2, secret_key = $3, testnet = $4,
hyperliquid_wallet_addr = $5, aster_user = $6, aster_signer = $7, aster_private_key = $8, updated_at = CURRENT_TIMESTAMP
WHERE id = $9 AND user_id = $10
`, 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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`, 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 *PostgreSQLDatabase) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error {
_, err := d.db.Exec(`
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO NOTHING
`, id, userID, name, provider, enabled, apiKey, customAPIURL)
return err
}
// CreateExchange 创建交易所配置
func (d *PostgreSQLDatabase) CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error {
_, 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)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
ON CONFLICT (id, user_id) DO NOTHING
`, id, userID, name, typ, enabled, apiKey, secretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey)
return err
}
// CreateTrader 创建交易员
func (d *PostgreSQLDatabase) 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 ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
`, 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 *PostgreSQLDatabase) 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, false) as use_coin_pool, COALESCE(use_oi_top, false) as use_oi_top,
COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, false) as override_base_prompt,
COALESCE(system_prompt_template, 'default') as system_prompt_template,
COALESCE(is_cross_margin, true) as is_cross_margin, created_at, updated_at
FROM traders WHERE user_id = $1 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 *PostgreSQLDatabase) UpdateTraderStatus(userID, id string, isRunning bool) error {
_, err := d.db.Exec(`UPDATE traders SET is_running = $1 WHERE id = $2 AND user_id = $3`, isRunning, id, userID)
return err
}
// UpdateTrader 更新交易员配置
func (d *PostgreSQLDatabase) UpdateTrader(trader *TraderRecord) error {
_, err := d.db.Exec(`
UPDATE traders SET
name = $1, ai_model_id = $2, exchange_id = $3, initial_balance = $4,
scan_interval_minutes = $5, btc_eth_leverage = $6, altcoin_leverage = $7,
trading_symbols = $8, custom_prompt = $9, override_base_prompt = $10,
system_prompt_template = $11, is_cross_margin = $12, updated_at = CURRENT_TIMESTAMP
WHERE id = $13 AND user_id = $14
`, 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 *PostgreSQLDatabase) UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error {
_, err := d.db.Exec(`UPDATE traders SET custom_prompt = $1, override_base_prompt = $2 WHERE id = $3 AND user_id = $4`, customPrompt, overrideBase, id, userID)
return err
}
// DeleteTrader 删除交易员
func (d *PostgreSQLDatabase) DeleteTrader(userID, id string) error {
_, err := d.db.Exec(`DELETE FROM traders WHERE id = $1 AND user_id = $2`, id, userID)
return err
}
// GetTraderConfig 获取交易员完整配置(包含AI模型和交易所信息)
func (d *PostgreSQLDatabase) 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 = $1 AND t.user_id = $2
`, traderID, userID).Scan(
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
&trader.CreatedAt, &trader.UpdatedAt,
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
&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 *PostgreSQLDatabase) GetSystemConfig(key string) (string, error) {
var value string
err := d.db.QueryRow(`SELECT value FROM system_config WHERE key = $1`, key).Scan(&value)
return value, err
}
// SetSystemConfig 设置系统配置
func (d *PostgreSQLDatabase) SetSystemConfig(key, value string) error {
_, err := d.db.Exec(`
INSERT INTO system_config (key, value) VALUES ($1, $2)
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = CURRENT_TIMESTAMP
`, key, value)
return err
}
// CreateUserSignalSource 创建用户信号源配置
func (d *PostgreSQLDatabase) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error {
_, err := d.db.Exec(`
INSERT INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, updated_at)
VALUES ($1, $2, $3, CURRENT_TIMESTAMP)
ON CONFLICT (user_id) DO UPDATE SET
coin_pool_url = $2, oi_top_url = $3, updated_at = CURRENT_TIMESTAMP
`, userID, coinPoolURL, oiTopURL)
return err
}
// GetUserSignalSource 获取用户信号源配置
func (d *PostgreSQLDatabase) 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 = $1
`, 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 *PostgreSQLDatabase) UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error {
_, err := d.db.Exec(`
UPDATE user_signal_sources SET coin_pool_url = $1, oi_top_url = $2, updated_at = CURRENT_TIMESTAMP
WHERE user_id = $3
`, coinPoolURL, oiTopURL, userID)
return err
}
// GetCustomCoins 获取所有交易员自定义币种
func (d *PostgreSQLDatabase) GetCustomCoins() []string {
var symbol string
var symbols []string
err := d.db.QueryRow(`
SELECT STRING_AGG(custom_coins, ',') as symbol
FROM traders WHERE custom_coins != ''
`).Scan(&symbol)
// 检测用户是否未配置币种 - 兼容性
if err != nil || 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
}
// LoadBetaCodesFromFile 从文件加载内测码到数据库
func (d *PostgreSQLDatabase) 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 INTO beta_codes (code) VALUES ($1) ON CONFLICT (code) DO NOTHING`)
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 *PostgreSQLDatabase) ValidateBetaCode(code string) (bool, error) {
var used bool
err := d.db.QueryRow(`SELECT used FROM beta_codes WHERE code = $1`, code).Scan(&used)
if err != nil {
if err == sql.ErrNoRows {
return false, nil // 内测码不存在
}
return false, err
}
return !used, nil // 内测码存在且未使用
}
// UseBetaCode 使用内测码(标记为已使用)
func (d *PostgreSQLDatabase) UseBetaCode(code, userEmail string) error {
result, err := d.db.Exec(`
UPDATE beta_codes SET used = true, used_by = $1, used_at = CURRENT_TIMESTAMP
WHERE code = $2 AND used = false
`, 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 *PostgreSQLDatabase) 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 = true`).Scan(&used)
if err != nil {
return 0, 0, err
}
return total, used, nil
}
// Close 关闭数据库连接
func (d *PostgreSQLDatabase) Close() error {
return d.db.Close()
}
-169
View File
@@ -1,169 +0,0 @@
-- PostgreSQL初始化脚本
-- AI交易系统数据库迁移
-- 用户表
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 FALSE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 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 FALSE,
api_key TEXT DEFAULT '',
custom_api_url TEXT DEFAULT '',
custom_model_name TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 交易所配置表
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, -- 'cex' or 'dex'
enabled BOOLEAN DEFAULT FALSE,
api_key TEXT DEFAULT '',
secret_key TEXT DEFAULT '',
testnet BOOLEAN DEFAULT FALSE,
-- Hyperliquid 特定字段
hyperliquid_wallet_addr TEXT DEFAULT '',
-- Aster 特定字段
aster_user TEXT DEFAULT '',
aster_signer TEXT DEFAULT '',
aster_private_key TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id, user_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- 用户信号源配置表
CREATE TABLE IF NOT EXISTS user_signal_sources (
id SERIAL PRIMARY KEY,
user_id TEXT NOT NULL,
coin_pool_url TEXT DEFAULT '',
oi_top_url TEXT DEFAULT '',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP 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 FALSE,
btc_eth_leverage INTEGER DEFAULT 5,
altcoin_leverage INTEGER DEFAULT 5,
trading_symbols TEXT DEFAULT '',
use_coin_pool BOOLEAN DEFAULT FALSE,
use_oi_top BOOLEAN DEFAULT FALSE,
custom_prompt TEXT DEFAULT '',
override_base_prompt BOOLEAN DEFAULT FALSE,
system_prompt_template TEXT DEFAULT 'default',
is_cross_margin BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP 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, user_id) REFERENCES exchanges(id, user_id)
);
-- 系统配置表
CREATE TABLE IF NOT EXISTS system_config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 内测码表
CREATE TABLE IF NOT EXISTS beta_codes (
code TEXT PRIMARY KEY,
used BOOLEAN DEFAULT FALSE,
used_by TEXT DEFAULT '',
used_at TIMESTAMP DEFAULT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 自动更新 updated_at 函数
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- 创建触发器:自动更新 updated_at
CREATE TRIGGER update_users_updated_at BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_ai_models_updated_at BEFORE UPDATE ON ai_models
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_exchanges_updated_at BEFORE UPDATE ON exchanges
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_traders_updated_at BEFORE UPDATE ON traders
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_user_signal_sources_updated_at BEFORE UPDATE ON user_signal_sources
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_system_config_updated_at BEFORE UPDATE ON system_config
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 插入默认数据
-- 初始化AI模型(使用default用户)
INSERT INTO ai_models (id, user_id, name, provider, enabled) VALUES
('deepseek', 'default', 'DeepSeek', 'deepseek', FALSE),
('qwen', 'default', 'Qwen', 'qwen', FALSE)
ON CONFLICT (id) DO NOTHING;
-- 初始化交易所(使用default用户)
INSERT INTO exchanges (id, user_id, name, type, enabled) VALUES
('binance', 'default', 'Binance Futures', 'binance', FALSE),
('hyperliquid', 'default', 'Hyperliquid', 'hyperliquid', FALSE),
('aster', 'default', 'Aster DEX', 'aster', FALSE)
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'),
('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;
-- 创建索引
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);
+2 -58
View File
@@ -1,44 +1,4 @@
services:
# PostgreSQL Database
postgres:
image: postgres:15-alpine
container_name: nofx-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-nofx}
POSTGRES_USER: ${POSTGRES_USER:-nofx}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-nofx123456}
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
ports:
- "${POSTGRES_PORT:-5433}:5432"
networks:
- nofx-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-nofx}"]
interval: 10s
timeout: 5s
retries: 5
# Redis Cache
redis:
image: redis:7-alpine
container_name: nofx-redis
restart: unless-stopped
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-redis123456}
volumes:
- redis_data:/data
ports:
- "${REDIS_PORT:-6380}:6379"
networks:
- nofx-network
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
# Backend service (API and core logic)
nofx:
build:
@@ -50,25 +10,13 @@ services:
- "${NOFX_BACKEND_PORT:-8080}:8080"
volumes:
- ./config.json:/app/config.json:ro
- ./config.db:/app/config.db
- ./beta_codes.txt:/app/beta_codes.txt:ro
- ./decision_logs:/app/decision_logs
- ./prompts:/app/prompts
- /etc/localtime:/etc/localtime:ro # Sync host time
environment:
- TZ=${NOFX_TIMEZONE:-Asia/Shanghai} # Set timezone
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_DB=${POSTGRES_DB:-nofx}
- POSTGRES_USER=${POSTGRES_USER:-nofx}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-nofx123456}
- REDIS_HOST=redis
- REDIS_PORT=6379
- REDIS_PASSWORD=${REDIS_PASSWORD:-redis123456}
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- nofx-network
healthcheck:
@@ -100,8 +48,4 @@ services:
networks:
nofx-network:
driver: bridge
volumes:
postgres_data:
redis_data:
driver: bridge
+1 -2
View File
@@ -9,8 +9,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.0
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/mattn/go-sqlite3 v1.14.16
github.com/pquerna/otp v1.4.0
github.com/sonirico/go-hyperliquid v0.17.0
golang.org/x/crypto v0.42.0
+2 -4
View File
@@ -107,8 +107,6 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
@@ -120,8 +118,8 @@ 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/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
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=
+2 -2
View File
@@ -41,7 +41,7 @@ type ConfigFile struct {
}
// syncConfigToDatabase 从config.json读取配置并同步到数据库
func syncConfigToDatabase(database config.DatabaseInterface) error {
func syncConfigToDatabase(database *config.Database) error {
// 检查config.json是否存在
if _, err := os.Stat("config.json"); os.IsNotExist(err) {
log.Printf("📄 config.json不存在,跳过同步")
@@ -110,7 +110,7 @@ func syncConfigToDatabase(database config.DatabaseInterface) error {
}
// loadBetaCodesToDatabase 加载内测码文件到数据库
func loadBetaCodesToDatabase(database config.DatabaseInterface) error {
func loadBetaCodesToDatabase(database *config.Database) error {
betaCodeFile := "beta_codes.txt"
// 检查内测码文件是否存在
+2 -2
View File
@@ -39,7 +39,7 @@ func NewTraderManager() *TraderManager {
}
// LoadTradersFromDatabase 从数据库加载所有交易员到内存
func (tm *TraderManager) LoadTradersFromDatabase(database config.DatabaseInterface) error {
func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) error {
tm.mu.Lock()
defer tm.mu.Unlock()
@@ -709,7 +709,7 @@ func containsUserPrefix(traderID string) bool {
}
// LoadUserTraders 为特定用户加载交易员到内存
func (tm *TraderManager) LoadUserTraders(database config.DatabaseInterface, userID string) error {
func (tm *TraderManager) LoadUserTraders(database *config.Database, userID string) error {
tm.mu.Lock()
defer tm.mu.Unlock()
-115
View File
@@ -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;
-49
View File
@@ -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;
-137
View File
@@ -1,137 +0,0 @@
#!/bin/bash
# PostgreSQL数据迁移脚本 - 一键迁移
# 用于将SQLite数据迁移到PostgreSQL
set -e
# 颜色定义
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# 检测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}"
echo "请安装 Docker Compose 或确保 Docker 支持 compose 子命令"
exit 1
fi
echo -e "${BLUE}🔄 开始数据库迁移...${NC}"
echo -e "${BLUE}📋 使用命令: ${DOCKER_COMPOSE_CMD}${NC}"
# 检查必要文件
if [ ! -f "migrate_actual_data.sql" ]; then
echo -e "${RED}❌ 错误:找不到 migrate_actual_data.sql 文件${NC}"
echo "请确保在项目根目录执行此脚本"
exit 1
fi
if [ ! -f "docker-compose.yml" ]; then
echo -e "${RED}❌ 错误:找不到 docker-compose.yml 文件${NC}"
echo "请确保在项目根目录执行此脚本"
exit 1
fi
# 停止现有服务(避免端口冲突)
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=12
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 5
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 migrate_actual_data.sql ${POSTGRES_CONTAINER}:/tmp/migrate_actual_data.sql
# 验证文件复制成功
if ! $DOCKER_COMPOSE_CMD exec postgres test -f /tmp/migrate_actual_data.sql; then
echo -e "${RED}❌ 迁移脚本复制失败${NC}"
exit 1
fi
# 执行数据迁移
echo -e "${BLUE}🔄 执行数据迁移...${NC}"
if $DOCKER_COMPOSE_CMD exec postgres env PAGER="" psql -U nofx -d nofx -f /tmp/migrate_actual_data.sql; then
echo -e "${GREEN}✅ 数据迁移成功!${NC}"
else
echo -e "${RED}❌ 数据迁移失败${NC}"
exit 1
fi
# 验证数据
echo -e "${BLUE}🔍 验证迁移结果...${NC}"
$DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -c "
SELECT '=== 数据库迁移验证 ===' as info;
SELECT
relname as \"表名\",
n_live_tup as \"记录数\"
FROM pg_stat_user_tables
WHERE n_live_tup > 0
ORDER BY relname;
"
# 显示系统配置(简化版本,避免长文本问题)
echo -e "${BLUE}📋 显示关键配置...${NC}"
$DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -c "
SELECT COUNT(*) as \"配置项总数\" FROM system_config;
SELECT 'admin_mode: ' || COALESCE((SELECT value FROM system_config WHERE key='admin_mode'), 'N/A') as \"管理员模式\";
SELECT 'beta_mode: ' || COALESCE((SELECT value FROM system_config WHERE key='beta_mode'), 'N/A') as \"内测模式\";
SELECT 'api_server_port: ' || COALESCE((SELECT value FROM system_config WHERE key='api_server_port'), 'N/A') as \"API端口\";
"
echo ""
echo -e "${GREEN}🎉 数据库迁移完成!${NC}"
echo ""
echo -e "${BLUE}📋 后续步骤:${NC}"
echo -e "1. 启动完整应用: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}"
echo -e "2. 验证功能: 访问 ${YELLOW}http://localhost:3000${NC}"
echo -e "3. 备份原SQLite: ${YELLOW}mv config.db config.db.backup${NC}"
echo ""
echo -e "${BLUE}🔧 如需回滚到SQLite:${NC}"
echo -e "1. 停止服务: ${YELLOW}$DOCKER_COMPOSE_CMD down${NC}"
echo -e "2. 删除环境变量: ${YELLOW}unset POSTGRES_HOST${NC} 或编辑 .env 文件"
echo -e "3. 恢复备份: ${YELLOW}mv config.db.backup config.db${NC}"
echo -e "4. 重启: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}"
echo ""
echo -e "${GREEN}🚀 PostgreSQL迁移成功!系统已升级到现代化数据库架构${NC}"
-207
View File
@@ -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;