mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-13 15:57:01 +08:00
Merge pull request #439 from NoFxAiOS/revert-411-beta
Revert "Beta feat: migrate from SQLite to PostgreSQL + Redis architecture"
This commit is contained in:
12
.env.example
12
.env.example
@@ -1,18 +1,6 @@
|
|||||||
# NOFX Environment Variables Template
|
# NOFX Environment Variables Template
|
||||||
# Copy this file to .env and modify the values as needed
|
# 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
|
# Ports Configuration
|
||||||
# Backend API server port (internal: 8080, external: configurable)
|
# Backend API server port (internal: 8080, external: configurable)
|
||||||
NOFX_BACKEND_PORT=8080
|
NOFX_BACKEND_PORT=8080
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ import (
|
|||||||
type Server struct {
|
type Server struct {
|
||||||
router *gin.Engine
|
router *gin.Engine
|
||||||
traderManager *manager.TraderManager
|
traderManager *manager.TraderManager
|
||||||
database config.DatabaseInterface
|
database *config.Database
|
||||||
port int
|
port int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer 创建API服务器
|
// 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模式(减少日志输出)
|
// 设置为Release模式(减少日志输出)
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -22,53 +21,8 @@ type Database struct {
|
|||||||
db *sql.DB
|
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 创建配置数据库
|
// NewDatabase 创建配置数据库
|
||||||
func NewDatabase(dbPath string) (DatabaseInterface, error) {
|
func NewDatabase(dbPath string) (*Database, error) {
|
||||||
// 检查是否启用PostgreSQL
|
|
||||||
if os.Getenv("POSTGRES_HOST") != "" {
|
|
||||||
// 使用PostgreSQL
|
|
||||||
pgDB, err := NewPostgreSQLDatabase()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("创建PostgreSQL数据库失败: %w", err)
|
|
||||||
}
|
|
||||||
return pgDB, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用SQLite(兼容模式)
|
|
||||||
db, err := sql.Open("sqlite3", dbPath)
|
db, err := sql.Open("sqlite3", dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("打开数据库失败: %w", err)
|
return nil, fmt.Errorf("打开数据库失败: %w", err)
|
||||||
|
|||||||
@@ -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
db/init.sql
169
db/init.sql
@@ -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);
|
|
||||||
@@ -1,44 +1,4 @@
|
|||||||
services:
|
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)
|
# Backend service (API and core logic)
|
||||||
nofx:
|
nofx:
|
||||||
build:
|
build:
|
||||||
@@ -50,25 +10,13 @@ services:
|
|||||||
- "${NOFX_BACKEND_PORT:-8080}:8080"
|
- "${NOFX_BACKEND_PORT:-8080}:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./config.json:/app/config.json:ro
|
- ./config.json:/app/config.json:ro
|
||||||
|
- ./config.db:/app/config.db
|
||||||
- ./beta_codes.txt:/app/beta_codes.txt:ro
|
- ./beta_codes.txt:/app/beta_codes.txt:ro
|
||||||
- ./decision_logs:/app/decision_logs
|
- ./decision_logs:/app/decision_logs
|
||||||
- ./prompts:/app/prompts
|
- ./prompts:/app/prompts
|
||||||
- /etc/localtime:/etc/localtime:ro # Sync host time
|
- /etc/localtime:/etc/localtime:ro # Sync host time
|
||||||
environment:
|
environment:
|
||||||
- TZ=${NOFX_TIMEZONE:-Asia/Shanghai} # Set timezone
|
- 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:
|
networks:
|
||||||
- nofx-network
|
- nofx-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -100,8 +48,4 @@ services:
|
|||||||
|
|
||||||
networks:
|
networks:
|
||||||
nofx-network:
|
nofx-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
redis_data:
|
|
||||||
3
go.mod
3
go.mod
@@ -9,8 +9,7 @@ require (
|
|||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/lib/pq v1.10.9
|
github.com/mattn/go-sqlite3 v1.14.16
|
||||||
github.com/mattn/go-sqlite3 v1.14.32
|
|
||||||
github.com/pquerna/otp v1.4.0
|
github.com/pquerna/otp v1.4.0
|
||||||
github.com/sonirico/go-hyperliquid v0.17.0
|
github.com/sonirico/go-hyperliquid v0.17.0
|
||||||
golang.org/x/crypto v0.42.0
|
golang.org/x/crypto v0.42.0
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -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/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
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 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
|
||||||
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
|
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=
|
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-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 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
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.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
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 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||||
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -41,7 +41,7 @@ type ConfigFile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// syncConfigToDatabase 从config.json读取配置并同步到数据库
|
// syncConfigToDatabase 从config.json读取配置并同步到数据库
|
||||||
func syncConfigToDatabase(database config.DatabaseInterface) error {
|
func syncConfigToDatabase(database *config.Database) error {
|
||||||
// 检查config.json是否存在
|
// 检查config.json是否存在
|
||||||
if _, err := os.Stat("config.json"); os.IsNotExist(err) {
|
if _, err := os.Stat("config.json"); os.IsNotExist(err) {
|
||||||
log.Printf("📄 config.json不存在,跳过同步")
|
log.Printf("📄 config.json不存在,跳过同步")
|
||||||
@@ -110,7 +110,7 @@ func syncConfigToDatabase(database config.DatabaseInterface) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadBetaCodesToDatabase 加载内测码文件到数据库
|
// loadBetaCodesToDatabase 加载内测码文件到数据库
|
||||||
func loadBetaCodesToDatabase(database config.DatabaseInterface) error {
|
func loadBetaCodesToDatabase(database *config.Database) error {
|
||||||
betaCodeFile := "beta_codes.txt"
|
betaCodeFile := "beta_codes.txt"
|
||||||
|
|
||||||
// 检查内测码文件是否存在
|
// 检查内测码文件是否存在
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func NewTraderManager() *TraderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadTradersFromDatabase 从数据库加载所有交易员到内存
|
// LoadTradersFromDatabase 从数据库加载所有交易员到内存
|
||||||
func (tm *TraderManager) LoadTradersFromDatabase(database config.DatabaseInterface) error {
|
func (tm *TraderManager) LoadTradersFromDatabase(database *config.Database) error {
|
||||||
tm.mu.Lock()
|
tm.mu.Lock()
|
||||||
defer tm.mu.Unlock()
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
@@ -709,7 +709,7 @@ func containsUserPrefix(traderID string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadUserTraders 为特定用户加载交易员到内存
|
// LoadUserTraders 为特定用户加载交易员到内存
|
||||||
func (tm *TraderManager) LoadUserTraders(database config.DatabaseInterface, userID string) error {
|
func (tm *TraderManager) LoadUserTraders(database *config.Database, userID string) error {
|
||||||
tm.mu.Lock()
|
tm.mu.Lock()
|
||||||
defer tm.mu.Unlock()
|
defer tm.mu.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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}"
|
|
||||||
@@ -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;
|
|
||||||
Reference in New Issue
Block a user