diff --git a/api/server.go b/api/server.go index 58b8211f..9750609a 100644 --- a/api/server.go +++ b/api/server.go @@ -998,7 +998,7 @@ func (s *Server) handleUpdateModelConfigs(c *gin.Context) { func (s *Server) handleGetExchangeConfigs(c *gin.Context) { userID := c.GetString("user_id") log.Printf("🔍 查询用户 %s 的交易所配置", userID) - exchanges, err := s.database.GetExchanges(userID) + exchanges, err := s.database.GetExchangesForAPI(userID) if err != nil { log.Printf("❌ 获取交易所配置失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("获取交易所配置失败: %v", err)}) @@ -1665,6 +1665,7 @@ func (s *Server) handleLogin(c *gin.Context) { // 验证密码 if !auth.CheckPassword(req.Password, user.PasswordHash) { + log.Printf("DEBUG: 密码验证失败") c.JSON(http.StatusUnauthorized, gin.H{"error": "邮箱或密码错误"}) return } @@ -1759,6 +1760,7 @@ func (s *Server) handleGetSupportedExchanges(c *gin.Context) { return } + c.JSON(http.StatusOK, exchanges) } diff --git a/config/database.go b/config/database.go index c3aa171d..df113db6 100644 --- a/config/database.go +++ b/config/database.go @@ -695,6 +695,48 @@ func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) { return exchanges, nil } +// GetExchangesForAPI 获取交易所配置(专用于API返回,排除敏感字段) +func (d *Database) GetExchangesForAPI(userID string) ([]*ExchangeConfig, error) { + rows, err := d.db.Query(` + SELECT id, user_id, name, type, enabled, + CASE + WHEN type = 'hyperliquid' THEN '' + ELSE COALESCE(api_key, '') + END as api_key, + '' as secret_key, + testnet, + COALESCE(hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr, + COALESCE(aster_user, '') as aster_user, + COALESCE(aster_signer, '') as aster_signer, + '' as aster_private_key, + created_at, updated_at + FROM exchanges WHERE user_id = ? ORDER BY id + `, userID) + if err != nil { + return nil, err + } + defer rows.Close() + + // 初始化为空切片而不是nil,确保JSON序列化为[]而不是null + exchanges := make([]*ExchangeConfig, 0) + for rows.Next() { + var exchange ExchangeConfig + err := rows.Scan( + &exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, + &exchange.Enabled, &exchange.APIKey, &exchange.SecretKey, &exchange.Testnet, + &exchange.HyperliquidWalletAddr, &exchange.AsterUser, + &exchange.AsterSigner, &exchange.AsterPrivateKey, + &exchange.CreatedAt, &exchange.UpdatedAt, + ) + if err != nil { + return nil, err + } + exchanges = append(exchanges, &exchange) + } + + return exchanges, nil +} + // UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置 func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error { log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled) diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend index 7bd02348..1145d501 100644 --- a/docker/Dockerfile.backend +++ b/docker/Dockerfile.backend @@ -47,9 +47,7 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN CGO_ENABLED=1 GOOS=linux \ - CGO_CFLAGS="-D_LARGEFILE64_SOURCE" \ - go build -trimpath -ldflags="-s -w" -o nofx . +RUN CGO_ENABLED=1 GOOS=linux CGO_CFLAGS="-D_LARGEFILE64_SOURCE" go build -trimpath -ldflags="-s -w" -o nofx . # ────────────────────────────────────────────────────────────── # Runtime Stage (Minimal Executable Environment) diff --git a/import_beta_codes.sh b/import_beta_codes.sh new file mode 100755 index 00000000..32cb6492 --- /dev/null +++ b/import_beta_codes.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# 将beta_codes.txt刷到PostgreSQL数据库 +echo "🎟️ 导入beta_codes.txt到数据库" + +# 检查文件 +if [ ! -f "beta_codes.txt" ]; then + echo "❌ 找不到beta_codes.txt文件" + exit 1 +fi + +# 检测docker命令 +if command -v "docker-compose" &> /dev/null; then + DOCKER_CMD="docker-compose" +else + DOCKER_CMD="docker compose" +fi + +# 统计数量 +TOTAL=$(cat beta_codes.txt | wc -l) +echo "📊 文件中共有 $TOTAL 个内测码" + +# 生成SQL +cat > import.sql << EOF +INSERT INTO beta_codes (code) VALUES +EOF + +# 读取每行并生成INSERT语句 +cat beta_codes.txt | while read line; do + if [ -n "$line" ]; then + echo "('$line')," >> import.sql + fi +done + +# 移除最后的逗号并添加冲突处理 +sed -i '$ s/,$//' import.sql +echo "ON CONFLICT (code) DO NOTHING;" >> import.sql + +# 执行导入 +echo "🔄 导入到数据库..." +$DOCKER_CMD exec -T postgres psql -U nofx -d nofx < import.sql + +echo "✅ 导入完成(重复的已跳过)" + +# 清理 +rm import.sql diff --git a/import_default_patch.sh b/import_default_patch.sh new file mode 100755 index 00000000..4a00d3d4 --- /dev/null +++ b/import_default_patch.sh @@ -0,0 +1,150 @@ +#!/bin/bash + +# 从SQLite导入default用户和系统默认数据到PostgreSQL +echo "🔧 从SQLite导入default用户和系统默认数据" +echo "========================================" + +# 检查SQLite数据库文件 +SQLITE_DB="config.db" +if [ ! -f "$SQLITE_DB" ]; then + echo "❌ 错误:找不到 SQLite 数据库文件 $SQLITE_DB" + exit 1 +fi + +# 检测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 "❌ 错误:找不到 docker-compose 或 docker compose 命令" + exit 1 +fi + +echo "📋 使用命令: $DOCKER_COMPOSE_CMD" + +# 分析SQLite中的default用户数据 +echo "📊 分析SQLite中的default用户数据..." +AI_MODEL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM ai_models WHERE user_id = 'default';" 2>/dev/null || echo "0") +EXCHANGE_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM exchanges WHERE user_id = 'default';" 2>/dev/null || echo "0") + +echo " 🤖 AI模型: $AI_MODEL_COUNT 个" +echo " 🏢 交易所: $EXCHANGE_COUNT 个" + +if [ "$AI_MODEL_COUNT" -eq 0 ] && [ "$EXCHANGE_COUNT" -eq 0 ]; then + echo "⚠️ SQLite中没有default用户的数据,将跳过导入" + exit 0 +fi + +# 生成导入脚本 +IMPORT_SQL="import_default_data.sql" + +cat > $IMPORT_SQL << EOL +-- 从SQLite导入default用户和系统默认数据 +-- 生成时间: $(date) + +-- 设置时区 +SET timezone = 'Asia/Shanghai'; + +-- 1. 创建default用户(如果不存在) +INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) +VALUES ('default', 'default@localhost', '', '', true, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) +ON CONFLICT (id) DO NOTHING; + +EOL + +# 导出AI模型数据 +if [ "$AI_MODEL_COUNT" -gt 0 ]; then + echo "🤖 导出AI模型数据..." + echo "-- AI模型数据 ($AI_MODEL_COUNT 条记录)" >> $IMPORT_SQL + echo "INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES" >> $IMPORT_SQL + + sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || quote(name) || ', ' || quote(provider) || ', ' || + CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(api_key, '')) || ', ' || quote(COALESCE(custom_api_url, '')) || ', ' || + quote(COALESCE(custom_model_name, '')) || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),' + FROM ai_models WHERE user_id = 'default';" | sed '$ s/,$//' >> $IMPORT_SQL + + echo "ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, provider = EXCLUDED.provider, enabled = EXCLUDED.enabled, api_key = EXCLUDED.api_key, custom_api_url = EXCLUDED.custom_api_url, custom_model_name = EXCLUDED.custom_model_name, updated_at = EXCLUDED.updated_at;" >> $IMPORT_SQL + echo "" >> $IMPORT_SQL +fi + +# 导出交易所数据 +if [ "$EXCHANGE_COUNT" -gt 0 ]; then + echo "🏢 导出交易所数据..." + echo "-- 交易所数据 ($EXCHANGE_COUNT 条记录)" >> $IMPORT_SQL + echo "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" >> $IMPORT_SQL + + sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || quote(name) || ', ' || quote(type) || ', ' || + CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(api_key, '')) || ', ' || quote(COALESCE(secret_key, '')) || ', ' || + CASE WHEN testnet = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(hyperliquid_wallet_addr, '')) || ', ' || + quote(COALESCE(aster_user, '')) || ', ' || quote(COALESCE(aster_signer, '')) || ', ' || quote(COALESCE(aster_private_key, '')) || ', ' || + quote(created_at) || ', ' || quote(updated_at) || '),' + FROM exchanges WHERE user_id = 'default';" | sed '$ s/,$//' >> $IMPORT_SQL + + echo "ON CONFLICT (id, user_id) DO UPDATE SET name = EXCLUDED.name, type = EXCLUDED.type, enabled = EXCLUDED.enabled, api_key = EXCLUDED.api_key, secret_key = EXCLUDED.secret_key, testnet = EXCLUDED.testnet, hyperliquid_wallet_addr = EXCLUDED.hyperliquid_wallet_addr, aster_user = EXCLUDED.aster_user, aster_signer = EXCLUDED.aster_signer, aster_private_key = EXCLUDED.aster_private_key, updated_at = EXCLUDED.updated_at;" >> $IMPORT_SQL + echo "" >> $IMPORT_SQL +fi + +# 添加验证查询 +cat >> $IMPORT_SQL << 'EOL' +-- 验证导入结果 +SELECT '=== 导入完成验证 ===' as status; +SELECT 'default用户' as item, COUNT(*) as count FROM users WHERE id = 'default' +UNION ALL SELECT 'AI模型', COUNT(*) FROM ai_models WHERE user_id = 'default' +UNION ALL SELECT '交易所', COUNT(*) FROM exchanges WHERE user_id = 'default'; +EOL + +echo "✅ 生成导入脚本: $IMPORT_SQL" + +# 确认执行 +echo +echo "⚠️ 准备导入default用户和系统默认数据,包括:" +echo " 1. default用户账户" +echo " 2. $AI_MODEL_COUNT 个AI模型" +echo " 3. $EXCHANGE_COUNT 个交易所" +echo +read -p "确认执行导入? (y/N): " confirm + +if [[ $confirm != [yY] ]]; then + echo "ℹ️ 已取消导入" + echo "手动执行命令: $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx -f /tmp/$IMPORT_SQL" + exit 0 +fi + +# 检查PostgreSQL连接 +echo "🔌 检查数据库连接..." +if ! $DOCKER_COMPOSE_CMD exec postgres pg_isready -U nofx > /dev/null 2>&1; then + echo "❌ PostgreSQL连接失败,请确保服务正在运行" + exit 1 +fi + +# 复制SQL脚本到容器 +echo "📦 复制导入脚本到容器..." +POSTGRES_CONTAINER=$($DOCKER_COMPOSE_CMD ps -q postgres) +if [ -z "$POSTGRES_CONTAINER" ]; then + echo "❌ 找不到PostgreSQL容器" + exit 1 +fi + +docker cp $IMPORT_SQL ${POSTGRES_CONTAINER}:/tmp/$IMPORT_SQL + +# 执行导入 +echo "🔄 执行数据导入..." +if $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -f /tmp/$IMPORT_SQL; then + echo + echo "✅ default用户和系统默认数据导入成功!" + echo + echo "📋 现在可以访问以下接口:" + echo " - GET /api/supported-models ($AI_MODEL_COUNT 个AI模型)" + echo " - GET /api/supported-exchanges ($EXCHANGE_COUNT 个交易所)" + echo + echo "🧹 清理导入文件..." + rm -f $IMPORT_SQL + $DOCKER_COMPOSE_CMD exec postgres rm -f /tmp/$IMPORT_SQL +else + echo "❌ 数据导入失败" + exit 1 +fi + +echo "🎉 导入完成!" diff --git a/migrate_to_postgres.sh b/migrate_to_postgres.sh new file mode 100755 index 00000000..b2a00406 --- /dev/null +++ b/migrate_to_postgres.sh @@ -0,0 +1,330 @@ +#!/bin/bash + +# 生产环境 SQLite -> PostgreSQL 数据迁移脚本 +# 真实数据迁移工具 - 支持完整数据导出和迁移 + +set -e + +# 颜色定义 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ 🚀 生产环境数据迁移工具 SQLite → PostgreSQL ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo + +# 检查必要文件 +SQLITE_DB="config.db" +if [ ! -f "$SQLITE_DB" ]; then + echo -e "${RED}❌ 错误:找不到 SQLite 数据库文件 $SQLITE_DB${NC}" + exit 1 +fi + +# 检测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}" + exit 1 +fi + +echo -e "${CYAN}📋 使用命令: ${DOCKER_COMPOSE_CMD}${NC}" + +# 分析当前SQLite数据 +echo -e "\n${BLUE}📊 分析当前SQLite数据库...${NC}" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# 获取数据统计 +USER_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM users;" 2>/dev/null || echo "0") +AI_MODEL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM ai_models;" 2>/dev/null || echo "0") +EXCHANGE_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM exchanges;" 2>/dev/null || echo "0") +TRADER_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM traders;" 2>/dev/null || echo "0") +CONFIG_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM system_config;" 2>/dev/null || echo "0") +BETA_CODE_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM beta_codes;" 2>/dev/null || echo "0") + +echo "📈 数据库表统计:" +echo " 👥 用户 (users): $USER_COUNT" +echo " 🤖 AI模型 (ai_models): $AI_MODEL_COUNT" +echo " 🏢 交易所 (exchanges): $EXCHANGE_COUNT" +echo " 🔧 交易员 (traders): $TRADER_COUNT" +echo " ⚙️ 系统配置 (system_config): $CONFIG_COUNT" +echo " 🎟️ 内测码 (beta_codes): $BETA_CODE_COUNT" + +# 检查是否有exchange_secrets表 +if sqlite3 $SQLITE_DB "SELECT name FROM sqlite_master WHERE type='table' AND name='exchange_secrets';" | grep -q exchange_secrets; then + SECRET_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM exchange_secrets;" 2>/dev/null || echo "0") + echo " 🔐 交易所密钥 (exchange_secrets): $SECRET_COUNT" +fi + +# 检查是否有user_signal_sources表 +if sqlite3 $SQLITE_DB "SELECT name FROM sqlite_master WHERE type='table' AND name='user_signal_sources';" | grep -q user_signal_sources; then + SIGNAL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM user_signal_sources;" 2>/dev/null || echo "0") + echo " 📡 用户信号源 (user_signal_sources): $SIGNAL_COUNT" +fi + +echo + +# 生成迁移时间戳 +TIMESTAMP=$(date '+%Y-%m-%d_%H-%M-%S') +MIGRATION_FILE="migrate_production_${TIMESTAMP}.sql" + +echo -e "${BLUE}📤 生成数据迁移脚本: $MIGRATION_FILE${NC}" + +# 生成SQL迁移脚本 +cat > $MIGRATION_FILE << EOL +-- 生产环境数据迁移脚本 +-- 从SQLite自动导出生成 +-- 执行时间: $(date) + +-- 设置时区 +SET timezone = 'Asia/Shanghai'; + +EOL + +# 导出用户数据 +if [ "$USER_COUNT" -gt 0 ]; then + echo -e "${CYAN}👥 导出用户数据...${NC}" + echo "-- 用户数据 ($USER_COUNT 条记录)" >> $MIGRATION_FILE + echo "INSERT INTO users (id, email, password_hash, otp_secret, otp_verified, created_at, updated_at) VALUES" >> $MIGRATION_FILE + + sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(COALESCE(email, '')) || ', ' || quote(COALESCE(password_hash, '')) || ', ' || quote(COALESCE(otp_secret, '')) || ', ' || + CASE WHEN otp_verified = 1 THEN 'true' ELSE 'false' END || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),' + FROM users;" | sed '$ s/,$//' >> $MIGRATION_FILE + + echo "ON CONFLICT (id) DO UPDATE SET email = EXCLUDED.email, password_hash = EXCLUDED.password_hash, otp_secret = EXCLUDED.otp_secret, otp_verified = EXCLUDED.otp_verified, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE + echo "" >> $MIGRATION_FILE +fi + +# 导出AI模型数据 +if [ "$AI_MODEL_COUNT" -gt 0 ]; then + echo -e "${CYAN}🤖 导出AI模型数据...${NC}" + echo "-- AI模型数据 ($AI_MODEL_COUNT 条记录)" >> $MIGRATION_FILE + echo "INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at) VALUES" >> $MIGRATION_FILE + + sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || + quote(name) || ', ' || quote(provider) || ', ' || + CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(api_key) || ', ' || quote(COALESCE(custom_api_url, '')) || ', ' || + quote(COALESCE(custom_model_name, '')) || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),' + FROM ai_models WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default' + AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE + + echo "ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, provider = EXCLUDED.provider, enabled = EXCLUDED.enabled, api_key = EXCLUDED.api_key, custom_api_url = EXCLUDED.custom_api_url, custom_model_name = EXCLUDED.custom_model_name, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE + echo "" >> $MIGRATION_FILE +fi + +# 导出交易所数据 +if [ "$EXCHANGE_COUNT" -gt 0 ]; then + echo -e "${CYAN}🏢 导出交易所数据...${NC}" + echo "-- 交易所数据 ($EXCHANGE_COUNT 条记录)" >> $MIGRATION_FILE + echo "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" >> $MIGRATION_FILE + + sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || + quote(name) || ', ' || quote(type) || ', ' || + CASE WHEN enabled = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(api_key, '')) || ', ' || quote(COALESCE(secret_key, '')) || ', ' || + CASE WHEN testnet = 1 THEN 'true' ELSE 'false' END || ', ' || quote(COALESCE(hyperliquid_wallet_addr, '')) || ', ' || + quote(COALESCE(aster_user, '')) || ', ' || quote(COALESCE(aster_signer, '')) || ', ' || quote(COALESCE(aster_private_key, '')) || ', ' || + quote(created_at) || ', ' || quote(updated_at) || '),' + FROM exchanges WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default' + AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE + + echo "ON CONFLICT (id, user_id) DO UPDATE SET name = EXCLUDED.name, type = EXCLUDED.type, enabled = EXCLUDED.enabled, api_key = EXCLUDED.api_key, secret_key = EXCLUDED.secret_key, testnet = EXCLUDED.testnet, hyperliquid_wallet_addr = EXCLUDED.hyperliquid_wallet_addr, aster_user = EXCLUDED.aster_user, aster_signer = EXCLUDED.aster_signer, aster_private_key = EXCLUDED.aster_private_key, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE + echo "" >> $MIGRATION_FILE +fi + +# 导出交易员数据 +if [ "$TRADER_COUNT" -gt 0 ]; then + echo -e "${CYAN}🔧 导出交易员数据...${NC}" + echo "-- 交易员数据 ($TRADER_COUNT 条记录)" >> $MIGRATION_FILE + echo "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" >> $MIGRATION_FILE + + sqlite3 $SQLITE_DB "SELECT '(' || quote(id) || ', ' || quote(user_id) || ', ' || + quote(name) || ', ' || quote(ai_model_id) || ', ' || + quote(exchange_id) || ', ' || initial_balance || ', ' || scan_interval_minutes || ', ' || + CASE WHEN is_running = 1 THEN 'true' ELSE 'false' END || ', ' || btc_eth_leverage || ', ' || altcoin_leverage || ', ' || + quote(COALESCE(trading_symbols, '')) || ', ' || + CASE WHEN use_coin_pool = 1 THEN 'true' ELSE 'false' END || ', ' || CASE WHEN use_oi_top = 1 THEN 'true' ELSE 'false' END || ', ' || + quote(COALESCE(custom_prompt, '')) || ', ' || CASE WHEN override_base_prompt = 1 THEN 'true' ELSE 'false' END || ', ' || + quote(COALESCE(system_prompt_template, 'default')) || ', ' || CASE WHEN is_cross_margin = 1 THEN 'true' ELSE 'false' END || ', ' || + quote(created_at) || ', ' || quote(updated_at) || '),' + FROM traders WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default' + AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE + + echo "ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, ai_model_id = EXCLUDED.ai_model_id, exchange_id = EXCLUDED.exchange_id, initial_balance = EXCLUDED.initial_balance, scan_interval_minutes = EXCLUDED.scan_interval_minutes, is_running = EXCLUDED.is_running, btc_eth_leverage = EXCLUDED.btc_eth_leverage, altcoin_leverage = EXCLUDED.altcoin_leverage, trading_symbols = EXCLUDED.trading_symbols, use_coin_pool = EXCLUDED.use_coin_pool, use_oi_top = EXCLUDED.use_oi_top, custom_prompt = EXCLUDED.custom_prompt, override_base_prompt = EXCLUDED.override_base_prompt, system_prompt_template = EXCLUDED.system_prompt_template, is_cross_margin = EXCLUDED.is_cross_margin, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE + echo "" >> $MIGRATION_FILE +fi + +# 导出系统配置 +if [ "$CONFIG_COUNT" -gt 0 ]; then + echo -e "${CYAN}⚙️ 导出系统配置...${NC}" + echo "-- 系统配置数据 ($CONFIG_COUNT 条记录)" >> $MIGRATION_FILE + echo "INSERT INTO system_config (key, value, updated_at) VALUES" >> $MIGRATION_FILE + + sqlite3 $SQLITE_DB "SELECT '(' || quote(key) || ', ' || quote(value) || ', ' || quote(updated_at) || '),' + FROM system_config;" | sed '$ s/,$//' >> $MIGRATION_FILE + + echo "ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE + echo "" >> $MIGRATION_FILE +fi + +# 导出内测码数据 +if [ "$BETA_CODE_COUNT" -gt 0 ]; then + echo -e "${CYAN}🎟️ 导出内测码数据...${NC}" + echo "-- 内测码数据 ($BETA_CODE_COUNT 条记录)" >> $MIGRATION_FILE + echo "INSERT INTO beta_codes (code, used, used_by, used_at, created_at) VALUES" >> $MIGRATION_FILE + + sqlite3 $SQLITE_DB "SELECT '(' || quote(code) || ', ' || CASE WHEN used = 1 THEN 'true' ELSE 'false' END || ', ' || + quote(COALESCE(used_by, '')) || ', ' || CASE WHEN used_at IS NULL THEN 'NULL' ELSE quote(used_at) END || ', ' || + quote(created_at) || '),' + FROM beta_codes;" | sed '$ s/,$//' >> $MIGRATION_FILE + + echo "ON CONFLICT (code) DO UPDATE SET used = EXCLUDED.used, used_by = EXCLUDED.used_by, used_at = EXCLUDED.used_at;" >> $MIGRATION_FILE + echo "" >> $MIGRATION_FILE +fi + +# 导出用户信号源(如果存在) +if sqlite3 $SQLITE_DB "SELECT name FROM sqlite_master WHERE type='table' AND name='user_signal_sources';" | grep -q user_signal_sources; then + SIGNAL_COUNT=$(sqlite3 $SQLITE_DB "SELECT COUNT(*) FROM user_signal_sources;" 2>/dev/null || echo "0") + if [ "$SIGNAL_COUNT" -gt 0 ]; then + echo -e "${CYAN}📡 导出用户信号源数据...${NC}" + echo "-- 用户信号源数据 ($SIGNAL_COUNT 条记录)" >> $MIGRATION_FILE + echo "INSERT INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, created_at, updated_at) VALUES" >> $MIGRATION_FILE + + sqlite3 $SQLITE_DB "SELECT '(' || quote(user_id) || ', ' || + quote(COALESCE(coin_pool_url, '')) || ', ' || + quote(COALESCE(oi_top_url, '')) || ', ' || quote(created_at) || ', ' || quote(updated_at) || '),' + FROM user_signal_sources WHERE user_id IS NOT NULL AND user_id != '' AND user_id != 'default' + AND user_id IN (SELECT id FROM users);" | sed '$ s/,$//' >> $MIGRATION_FILE + + echo "ON CONFLICT (user_id) DO UPDATE SET coin_pool_url = EXCLUDED.coin_pool_url, oi_top_url = EXCLUDED.oi_top_url, updated_at = EXCLUDED.updated_at;" >> $MIGRATION_FILE + echo "" >> $MIGRATION_FILE + fi +fi + +# 添加迁移验证查询 +cat >> $MIGRATION_FILE << 'EOL' +-- 迁移验证查询 +SELECT '=== 数据迁移完成验证 ===' as status; +SELECT 'users' as table_name, COUNT(*) as record_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 +UNION ALL SELECT 'user_signal_sources', COUNT(*) FROM user_signal_sources +ORDER BY table_name; + +-- 显示关键配置 +SELECT '=== 关键系统配置 ===' as info; +SELECT key, + CASE WHEN LENGTH(value) > 50 THEN LEFT(value, 50) || '...' ELSE value END as value +FROM system_config +WHERE key IN ('admin_mode', 'beta_mode', 'api_server_port', 'default_coins', 'jwt_secret') +ORDER BY key; +EOL + +echo -e "${GREEN}✅ 迁移脚本生成完成: $MIGRATION_FILE${NC}" + +# 确认是否执行迁移 +echo +echo -e "${YELLOW}⚠️ 准备执行数据迁移,这将:${NC}" +echo " 1. 停止现有服务" +echo " 2. 启动PostgreSQL和Redis" +echo " 3. 执行数据迁移" +echo " 4. 验证迁移结果" +echo +read -p "确认执行迁移? (y/N): " confirm + +if [[ $confirm != [yY] ]]; then + echo -e "${BLUE}ℹ️ 迁移脚本已生成,可稍后手动执行${NC}" + echo "手动执行命令: $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx -f /tmp/$MIGRATION_FILE" + exit 0 +fi + +# 执行迁移 +echo -e "\n${BLUE}🚀 开始执行数据迁移...${NC}" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# 停止现有服务 +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=15 +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 3 + 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 $MIGRATION_FILE ${POSTGRES_CONTAINER}:/tmp/$MIGRATION_FILE + +# 验证文件复制成功 +if ! $DOCKER_COMPOSE_CMD exec postgres test -f /tmp/$MIGRATION_FILE; then + echo -e "${RED}❌ 迁移脚本复制失败${NC}" + exit 1 +fi + +# 执行数据迁移 +echo -e "${BLUE}🔄 执行数据迁移...${NC}" +if $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -f /tmp/$MIGRATION_FILE; then + echo -e "${GREEN}✅ 数据迁移成功!${NC}" +else + echo -e "${RED}❌ 数据迁移失败${NC}" + echo "查看错误日志: $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx -c \"SELECT version();\"" + exit 1 +fi + +echo +echo -e "${GREEN}🎉 生产环境数据迁移完成!${NC}" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo -e "${BLUE}📋 后续步骤:${NC}" +echo -e "1. 备份原SQLite: ${YELLOW}mv config.db config.db.backup.$(date +%Y%m%d)${NC}" +echo -e "2. 启动完整应用: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}" +echo -e "3. 验证功能: 访问 ${YELLOW}http://localhost:3000${NC}" +echo -e "4. 删除迁移文件: ${YELLOW}rm $MIGRATION_FILE${NC}" +echo +echo -e "${BLUE}🔧 如需回滚:${NC}" +echo -e "1. 停止服务: ${YELLOW}$DOCKER_COMPOSE_CMD down${NC}" +echo -e "2. 恢复SQLite: ${YELLOW}mv config.db.backup.$(date +%Y%m%d) config.db${NC}" +echo -e "3. 删除环境变量或编辑 .env 文件注释掉 POSTGRES_HOST" +echo -e "4. 重启: ${YELLOW}$DOCKER_COMPOSE_CMD up${NC}" +echo +echo -e "${GREEN}🚀 PostgreSQL生产环境迁移成功!${NC}" diff --git a/view_pg_data.sh b/view_pg_data.sh index 59a7aef5..1ae89206 100755 --- a/view_pg_data.sh +++ b/view_pg_data.sh @@ -59,7 +59,15 @@ GROUP BY used ORDER BY used; " +echo -e "\n📝 未使用的内测码:" +$DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -c " +SELECT code +FROM beta_codes +WHERE used = false +ORDER BY created_at DESC; +" + echo -e "\n👥 用户信息:" $DOCKER_COMPOSE_CMD exec postgres psql -U nofx -d nofx --pset pager=off -c " SELECT id, email, otp_verified, created_at FROM users ORDER BY created_at; -" \ No newline at end of file +" diff --git a/web/src/components/AITradersPage.tsx b/web/src/components/AITradersPage.tsx index b691bb2f..417292de 100644 --- a/web/src/components/AITradersPage.tsx +++ b/web/src/components/AITradersPage.tsx @@ -1092,6 +1092,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) { {showExchangeModal && ( e.id === selectedExchangeId - ) + // 获取当前选择的交易所信息 + // 编辑模式:从 configuredExchanges 查找(包含用户配置的 apiKey、secretKey 等) + // 新增模式:从 allExchanges 查找(系统支持的交易所列表) + const selectedExchange = editingExchangeId + ? configuredExchanges?.find(e => e.id === selectedExchangeId) + : allExchanges?.find(e => e.id === selectedExchangeId); // 如果是编辑现有交易所,初始化表单数据 useEffect(() => { @@ -1618,6 +1626,9 @@ function ExchangeConfigModal({ setPassphrase('') // Don't load existing passphrase for security setTestnet(selectedExchange.testnet || false) + // Hyperliquid 字段 + setHyperliquidWalletAddr(selectedExchange.hyperliquidWalletAddr || '') + // Aster 字段 setAsterUser(selectedExchange.asterUser || '') setAsterSigner(selectedExchange.asterSigner || '') @@ -1660,7 +1671,7 @@ function ExchangeConfigModal({ await onSave(selectedExchangeId, apiKey.trim(), secretKey.trim(), testnet) } else if (selectedExchange?.id === 'hyperliquid') { if (!apiKey.trim()) return // 只验证私钥,钱包地址自动从私钥生成 - await onSave(selectedExchangeId, apiKey.trim(), '', testnet, '') // 传空字符串,后端自动生成地址 + await onSave(selectedExchangeId, apiKey.trim(), '', testnet, hyperliquidWalletAddr.trim() || '') // 保留现有钱包地址 } else if (selectedExchange?.id === 'aster') { if (!asterUser.trim() || !asterSigner.trim() || !asterPrivateKey.trim()) return @@ -2070,6 +2081,30 @@ function ExchangeConfigModal({ {t('hyperliquidPrivateKeyDesc', language)} + +
+ + setHyperliquidWalletAddr(e.target.value)} + placeholder="钱包地址(可选,通常由私钥自动生成)" + className="w-full px-3 py-2 rounded" + style={{ + background: '#0B0E11', + border: '1px solid #2B3139', + color: '#EAECEF', + }} + /> +
+ 钱包地址通常由私钥自动生成,编辑时可查看或修改 +
+
)}