mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2025-12-06 13:54:41 +08:00
* feat(trader): add LIGHTER DEX integration (initial implementation) Add pure Go implementation of LIGHTER DEX trader following NOFX architecture Features: - ✅ Account management with Ethereum wallet authentication - ✅ Order operations: market/limit orders, cancel, query - ✅ Position & balance queries - ✅ Zero-fee trading support (Standard accounts) - ✅ Up to 50x leverage for BTC/ETH Implementation: - Pure Go (no CGO dependencies) for easy deployment - Based on hyperliquid_trader.go architecture - Uses Ethereum ECDSA signatures (like Hyperliquid) - API base URL: https://mainnet.zklighter.elliot.ai Files: - lighter_trader.go: Core trader structure & auth - lighter_orders.go: Order management (create/cancel/query) - lighter_account.go: Balance & position queries Status: ⚠️ Partial implementation - ✅ Core structure complete - ⏸️ Auth token generation needs implementation - ⏸️ Transaction signing logic needs completion - ⏸️ Config integration pending Next steps: 1. Complete auth token generation 2. Add to config/exchange registry 3. Add frontend UI support 4. Create test suite 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat: Add LIGHTER DEX integration (快速整合階段) ## 🚀 新增功能 - ✅ 添加 LIGHTER DEX 作為第四個支持的交易所 (Binance, Hyperliquid, Aster, LIGHTER) - ✅ 完整的數據庫配置支持(ExchangeConfig 新增 LighterWalletAddr, LighterPrivateKey 字段) - ✅ 交易所註冊與初始化(initDefaultData 註冊 "lighter") - ✅ TraderManager 集成(配置傳遞邏輯完成) - ✅ AutoTrader 支持(NewAutoTrader 添加 "lighter" case) ## 📝 實現細節 ### 後端整合 1. **數據庫層** (config/database.go): - ExchangeConfig 添加 LIGHTER 字段 - 創建表時添加 lighter_wallet_addr, lighter_private_key 欄位 - ALTER TABLE 語句用於向後兼容 - UpdateExchange/CreateExchange/GetExchanges 支持 LIGHTER - migrateExchangesTable 支持 LIGHTER 字段 2. **API 層** (api/server.go, api/utils.go): - UpdateExchangeConfigRequest 添加 LIGHTER 字段 - SanitizeExchangeConfigForLog 添加脫敏處理 3. **Trader 層** (trader/): - lighter_trader.go: 核心結構、認證、初始化 - lighter_account.go: 餘額、持倉、市場價格查詢 - lighter_orders.go: 訂單管理(創建、取消、查詢) - lighter_trading.go: 交易功能實現(開多/空、平倉、止損/盈) - 實現完整 Trader interface (13個方法) 4. **Manager 層** (manager/trader_manager.go): - addTraderFromDB 添加 LIGHTER 配置設置 - AutoTraderConfig 添加 LIGHTER 字段 ### 實現的功能(快速整合階段) ✅ 基礎交易功能 (OpenLong, OpenShort, CloseLong, CloseShort) ✅ 餘額查詢 (GetBalance, GetAccountBalance) ✅ 持倉查詢 (GetPositions, GetPosition) ✅ 訂單管理 (CreateOrder, CancelOrder, CancelAllOrders) ✅ 止損/止盈 (SetStopLoss, SetTakeProfit, CancelStopLossOrders) ✅ 市場數據 (GetMarketPrice) ✅ 格式化工具 (FormatQuantity) ## ⚠️ TODO(完整實現階段) - [ ] 完整認證令牌生成邏輯 (refreshAuthToken) - [ ] 完整交易簽名邏輯(參考 Python SDK) - [ ] 從 API 獲取幣種精度 - [ ] 區分止損/止盈訂單類型 - [ ] 前端 UI 支持 - [ ] 完整測試套件 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat: 完整集成 LIGHTER DEX with SDK - 集成官方 lighter-go SDK (v0.0.0-20251104171447-78b9b55ebc48) - 集成 Poseidon2 Goldilocks 簽名庫 (poseidon_crypto v0.0.11) - 實現完整的 LighterTraderV2 使用官方 SDK - 實現 17 個 Trader 接口方法(賬戶、交易、訂單管理) - 支持雙密鑰系統(L1 錢包 + API Key) - V1/V2 自動切換機制(向後兼容) - 自動認證令牌管理(8小時有效期) - 添加完整集成文檔 LIGHTER_INTEGRATION.md 新增文件: - trader/lighter_trader_v2.go - V2 核心結構和初始化 - trader/lighter_trader_v2_account.go - 賬戶查詢方法 - trader/lighter_trader_v2_trading.go - 交易操作方法 - trader/lighter_trader_v2_orders.go - 訂單管理方法 - LIGHTER_INTEGRATION.md - 完整文檔 修改文件: - trader/auto_trader.go - 添加 LighterAPIKeyPrivateKey 配置 - config/database.go - 添加 API Key 字段支持 - go.mod, go.sum - 添加 SDK 依賴 🤖 Generated with Claude Code Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(lighter): 實現完整 HTTP 調用與動態市場映射 ### 實現的功能 #### 1. submitOrder() - 真實訂單提交 - 使用 POST /api/v1/sendTx 提交已簽名訂單 - tx_type: 14 (CREATE_ORDER) - 價格保護機制 (price_protection) - 完整錯誤處理與響應解析 #### 2. GetActiveOrders() - 查詢活躍訂單 - GET /api/v1/accountActiveOrders - 使用認證令牌 (Authorization header) - 支持按市場索引過濾 #### 3. CancelOrder() - 真實取消訂單 - 使用 SDK 簽名 CancelOrderTxReq - POST /api/v1/sendTx with tx_type: 15 (CANCEL_ORDER) - 自動 nonce 管理 #### 4. getMarketIndex() - 動態市場映射 - 從 GET /api/v1/orderBooks 獲取市場列表 - 內存緩存 (marketIndexMap) 提高性能 - 回退到硬編碼映射(API 失敗時) - 線程安全 (sync.RWMutex) ### 技術實現 **數據結構**: - SendTxRequest/SendTxResponse - sendTx 請求響應 - MarketInfo - 市場信息緩存 **並發安全**: - marketMutex - 保護市場索引緩存 - 讀寫鎖優化性能 **錯誤處理**: - API 失敗回退機制 - 詳細日誌記錄 - HTTP 狀態碼驗證 ### 測試 ✅ 編譯通過 (CGO_ENABLED=1) ✅ 所有 Trader 接口方法實現完整 ✅ HTTP 調用格式符合 LIGHTER API 規範 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * feat(lighter): 數據庫遷移與前端類型支持 ### 數據庫變更 #### 新增欄位 - `exchanges.lighter_api_key_private_key` TEXT DEFAULT '' - 支持 LIGHTER V2 的 40 字節 API Key 私鑰 #### 遷移腳本 - 📄 `migrations/002_add_lighter_api_key.sql` - 包含完整的驗證和統計查詢 - 向後兼容現有配置(默認為空,使用 V1) #### Schema 更新 - `config/database.go`: - 更新 CREATE TABLE 語句 - 更新 exchanges_new 表結構 - 新增 ALTER TABLE 遷移命令 ### 前端類型更新 #### types.ts - 新增 `Exchange` 接口字段: - `lighterWalletAddr?: string` - L1 錢包地址 - `lighterPrivateKey?: string` - L1 私鑰 - `lighterApiKeyPrivateKey?: string` - API Key 私鑰(⭐新增) ### 技術細節 **數據庫兼容性**: - 使用 ALTER TABLE ADD COLUMN IF NOT EXISTS - 默認值為空字符串 - 不影響現有數據 **類型安全**: - TypeScript 可選字段 - 與後端 ExchangeConfig 結構對齊 ### 下一步 ⏳ **待完成**: 1. ExchangeConfigModal 組件更新 2. API 調用參數傳遞 3. V1/V2 狀態顯示 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * docs(lighter): 更新 LIGHTER_INTEGRATION.md 文檔狀態 * feat(lighter): 前端完整實現 - API Key 配置與 V1/V2 狀態 **英文**: - `lighterWalletAddress`, `lighterPrivateKey`, `lighterApiKeyPrivateKey` - `lighterWalletAddressDesc`, `lighterPrivateKeyDesc`, `lighterApiKeyPrivateKeyDesc` - `lighterApiKeyOptionalNote` - V1 模式提示 - `lighterV1Description`, `lighterV2Description` - 狀態說明 - `lighterPrivateKeyImported` - 導入成功提示 **中文(繁體)**: - 完整的中文翻譯對應 - 專業術語保留原文(L1、API Key、Poseidon2) **Exchange 接口**: - `lighterWalletAddr?: string` - `lighterPrivateKey?: string` - `lighterApiKeyPrivateKey?: string` **UpdateExchangeConfigRequest 接口**: - `lighter_wallet_addr?: string` - `lighter_private_key?: string` - `lighter_api_key_private_key?: string` **狀態管理**: - 添加 3 個 LIGHTER 狀態變量 - 更新 `secureInputTarget` 類型包含 'lighter' **表單字段**: - L1 錢包地址(必填,text input) - L1 私鑰(必填,password + 安全輸入) - API Key 私鑰(可選,password,40 字節) **V1/V2 狀態顯示**: - 動態背景顏色(V1: 橙色 #3F2E0F,V2: 綠色 #0F3F2E) - 圖標指示(V1: ⚠️,V2: ✅) - 狀態說明文字 **驗證邏輯**: - 必填字段:錢包地址 + L1 私鑰 - API Key 為可選字段 - 自動 V1/V2 檢測 **安全輸入**: - 支持通過 TwoStageKeyModal 安全導入私鑰 - 導入成功後顯示 toast 提示 **handleSaveExchange**: - 添加 3 個 LIGHTER 參數 - 更新交易所對象(新增/更新) - 構建 API 請求(snake_case 字段) **V1 模式(無 API Key)**: ``` ┌────────────────────────────────────────┐ │ ⚠️ LIGHTER V1 │ │ 基本模式 - 功能受限,僅用於測試框架 │ └────────────────────────────────────────┘ 背景: #3F2E0F (橙色調) 邊框: #F59E0B (橙色) ``` **V2 模式(有 API Key)**: ``` ┌────────────────────────────────────────┐ │ ✅ LIGHTER V2 │ │ 完整模式 - 支持 Poseidon2 簽名和真實交易 │ └────────────────────────────────────────┘ 背景: #0F3F2E (綠色調) 邊框: #10B981 (綠色) ``` 1. **類型安全** - 完整的 TypeScript 類型定義 - Props 接口正確對齊 - ✅ 無 LIGHTER 相關編譯錯誤 2. **用戶體驗** - 清晰的必填/可選字段區分 - 實時 V1/V2 狀態反饋 - 安全私鑰輸入支持 3. **向後兼容** - 不影響現有交易所配置 - 所有字段為可選(Optional) - API 請求格式統一 ✅ TypeScript 編譯通過(無 LIGHTER 錯誤) ✅ 類型定義完整且正確 ✅ 所有必需文件已更新 ✅ 與後端 API 格式對齊 Modified: - `web/src/i18n/translations.ts` - 中英文翻譯 - `web/src/types.ts` - 類型定義 - `web/src/components/traders/ExchangeConfigModal.tsx` - Modal 組件 - `web/src/hooks/useTraderActions.ts` - Actions hook 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> * test(lighter): 添加 V1 測試套件與修復 SafeFloat64 缺失 - 新增 trader/helpers.go: 添加 SafeFloat64/SafeString/SafeInt 輔助函數 - 新增 trader/lighter_trader_test.go: LIGHTER V1 測試套件 - ✅ 測試通過 (7/10): - NewTrader 驗證 (無效私鑰, 有效私鑰格式) - FormatQuantity - GetExchangeType - InvalidQuantity 驗證 - InvalidLeverage 驗證 - HelperFunctions (SafeFloat64) - ⚠️ 待改進 (3/10): - GetBalance (需要調整 mock 響應格式) - GetPositions (需要調整 mock 響應格式) - GetMarketPrice (需要調整 mock 響應格式) - 修復 Bug: lighter_account.go 和 lighter_trader_v2_account.go 中未定義的 SafeFloat64 - 測試框架: httptest.Server mock LIGHTER API - 安全: 使用固定測試私鑰 (不含真實資金) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: tinkle-community <tinklefund@gmail.com> --------- Co-authored-by: the-dev-z <the-dev-z@users.noreply.github.com> Co-authored-by: tinkle-community <tinklefund@gmail.com>
1316 lines
47 KiB
Go
1316 lines
47 KiB
Go
package config
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"database/sql"
|
||
"encoding/base32"
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"nofx/crypto"
|
||
"nofx/market"
|
||
"os"
|
||
"slices"
|
||
"strings"
|
||
"time"
|
||
|
||
_ "modernc.org/sqlite"
|
||
)
|
||
|
||
// DatabaseInterface 定义了数据库实现需要提供的方法集合
|
||
type DatabaseInterface interface {
|
||
SetCryptoService(cs *crypto.CryptoService)
|
||
CreateUser(user *User) error
|
||
GetUserByEmail(email string) (*User, error)
|
||
GetUserByID(userID string) (*User, error)
|
||
GetAllUsers() ([]string, error)
|
||
UpdateUserOTPVerified(userID string, verified bool) error
|
||
GetAIModels(userID string) ([]*AIModelConfig, error)
|
||
UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error
|
||
GetExchanges(userID string) ([]*ExchangeConfig, error)
|
||
UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey string) error
|
||
CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error
|
||
CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error
|
||
CreateTrader(trader *TraderRecord) error
|
||
GetTraders(userID string) ([]*TraderRecord, error)
|
||
UpdateTraderStatus(userID, id string, isRunning bool) error
|
||
UpdateTrader(trader *TraderRecord) error
|
||
UpdateTraderInitialBalance(userID, id string, newBalance float64) error
|
||
UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error
|
||
DeleteTrader(userID, id string) error
|
||
GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error)
|
||
GetSystemConfig(key string) (string, error)
|
||
SetSystemConfig(key, value string) error
|
||
CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error
|
||
GetUserSignalSource(userID string) (*UserSignalSource, error)
|
||
UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error
|
||
GetCustomCoins() []string
|
||
LoadBetaCodesFromFile(filePath string) error
|
||
ValidateBetaCode(code string) (bool, error)
|
||
UseBetaCode(code, userEmail string) error
|
||
GetBetaCodeStats() (total, used int, err error)
|
||
Close() error
|
||
}
|
||
|
||
// Database 配置数据库
|
||
type Database struct {
|
||
db *sql.DB
|
||
cryptoService *crypto.CryptoService
|
||
}
|
||
|
||
// NewDatabase 创建配置数据库
|
||
func NewDatabase(dbPath string) (*Database, error) {
|
||
db, err := sql.Open("sqlite", dbPath)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("打开数据库失败: %w", err)
|
||
}
|
||
|
||
// 🔒 启用 WAL 模式,提高并发性能和崩溃恢复能力
|
||
// WAL (Write-Ahead Logging) 模式的优势:
|
||
// 1. 更好的并发性能:读操作不会被写操作阻塞
|
||
// 2. 崩溃安全:即使在断电或强制终止时也能保证数据完整性
|
||
// 3. 更快的写入:不需要每次都写入主数据库文件
|
||
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
|
||
db.Close()
|
||
return nil, fmt.Errorf("启用WAL模式失败: %w", err)
|
||
}
|
||
|
||
// 🔒 设置 synchronous=FULL 确保数据持久性
|
||
// FULL (2) 模式: 确保数据在关键时刻完全写入磁盘
|
||
// 配合 WAL 模式,在保证数据安全的同时获得良好性能
|
||
if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil {
|
||
db.Close()
|
||
return nil, fmt.Errorf("设置synchronous失败: %w", err)
|
||
}
|
||
|
||
database := &Database{db: db}
|
||
if err := database.createTables(); err != nil {
|
||
return nil, fmt.Errorf("创建表失败: %w", err)
|
||
}
|
||
|
||
if err := database.initDefaultData(); err != nil {
|
||
return nil, fmt.Errorf("初始化默认数据失败: %w", err)
|
||
}
|
||
|
||
log.Printf("✅ 数据库已启用 WAL 模式和 FULL 同步,数据持久性得到保证")
|
||
return database, nil
|
||
}
|
||
|
||
// createTables 创建数据库表
|
||
func (d *Database) createTables() error {
|
||
queries := []string{
|
||
// AI模型配置表
|
||
`CREATE TABLE IF NOT EXISTS ai_models (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT NOT NULL,
|
||
provider TEXT NOT NULL,
|
||
enabled BOOLEAN DEFAULT 0,
|
||
api_key TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||
)`,
|
||
|
||
// 交易所配置表
|
||
`CREATE TABLE IF NOT EXISTS exchanges (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT NOT NULL,
|
||
type TEXT NOT NULL, -- 'cex' or 'dex'
|
||
enabled BOOLEAN DEFAULT 0,
|
||
api_key TEXT DEFAULT '',
|
||
secret_key TEXT DEFAULT '',
|
||
testnet BOOLEAN DEFAULT 0,
|
||
-- Hyperliquid 特定字段
|
||
hyperliquid_wallet_addr TEXT DEFAULT '',
|
||
-- Aster 特定字段
|
||
aster_user TEXT DEFAULT '',
|
||
aster_signer TEXT DEFAULT '',
|
||
aster_private_key TEXT DEFAULT '',
|
||
-- LIGHTER 特定字段
|
||
lighter_wallet_addr TEXT DEFAULT '',
|
||
lighter_private_key TEXT DEFAULT '',
|
||
lighter_api_key_private_key TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||
)`,
|
||
|
||
// 用户信号源配置表
|
||
`CREATE TABLE IF NOT EXISTS user_signal_sources (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id TEXT NOT NULL,
|
||
coin_pool_url TEXT DEFAULT '',
|
||
oi_top_url TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
UNIQUE(user_id)
|
||
)`,
|
||
|
||
// 交易员配置表
|
||
`CREATE TABLE IF NOT EXISTS traders (
|
||
id TEXT PRIMARY KEY,
|
||
user_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT NOT NULL,
|
||
ai_model_id TEXT NOT NULL,
|
||
exchange_id TEXT NOT NULL,
|
||
initial_balance REAL NOT NULL,
|
||
scan_interval_minutes INTEGER DEFAULT 3,
|
||
is_running BOOLEAN DEFAULT 0,
|
||
btc_eth_leverage INTEGER DEFAULT 5,
|
||
altcoin_leverage INTEGER DEFAULT 5,
|
||
trading_symbols TEXT DEFAULT '',
|
||
use_coin_pool BOOLEAN DEFAULT 0,
|
||
use_oi_top BOOLEAN DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (ai_model_id) REFERENCES ai_models(id),
|
||
FOREIGN KEY (exchange_id) REFERENCES exchanges(id)
|
||
)`,
|
||
|
||
// 用户表
|
||
`CREATE TABLE IF NOT EXISTS users (
|
||
id TEXT PRIMARY KEY,
|
||
email TEXT UNIQUE NOT NULL,
|
||
password_hash TEXT NOT NULL,
|
||
otp_secret TEXT,
|
||
otp_verified BOOLEAN DEFAULT 0,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)`,
|
||
|
||
// 系统配置表
|
||
`CREATE TABLE IF NOT EXISTS system_config (
|
||
key TEXT PRIMARY KEY,
|
||
value TEXT NOT NULL,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)`,
|
||
|
||
// 内测码表
|
||
`CREATE TABLE IF NOT EXISTS beta_codes (
|
||
code TEXT PRIMARY KEY,
|
||
used BOOLEAN DEFAULT 0,
|
||
used_by TEXT DEFAULT '',
|
||
used_at DATETIME DEFAULT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)`,
|
||
|
||
// 触发器:自动更新 updated_at
|
||
`CREATE TRIGGER IF NOT EXISTS update_users_updated_at
|
||
AFTER UPDATE ON users
|
||
BEGIN
|
||
UPDATE users SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||
END`,
|
||
|
||
`CREATE TRIGGER IF NOT EXISTS update_ai_models_updated_at
|
||
AFTER UPDATE ON ai_models
|
||
BEGIN
|
||
UPDATE ai_models SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||
END`,
|
||
|
||
`CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at
|
||
AFTER UPDATE ON exchanges
|
||
BEGIN
|
||
UPDATE exchanges SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||
END`,
|
||
|
||
`CREATE TRIGGER IF NOT EXISTS update_traders_updated_at
|
||
AFTER UPDATE ON traders
|
||
BEGIN
|
||
UPDATE traders SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||
END`,
|
||
|
||
`CREATE TRIGGER IF NOT EXISTS update_user_signal_sources_updated_at
|
||
AFTER UPDATE ON user_signal_sources
|
||
BEGIN
|
||
UPDATE user_signal_sources SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
||
END`,
|
||
|
||
`CREATE TRIGGER IF NOT EXISTS update_system_config_updated_at
|
||
AFTER UPDATE ON system_config
|
||
BEGIN
|
||
UPDATE system_config SET updated_at = CURRENT_TIMESTAMP WHERE key = NEW.key;
|
||
END`,
|
||
}
|
||
|
||
for _, query := range queries {
|
||
if _, err := d.db.Exec(query); err != nil {
|
||
return fmt.Errorf("执行SQL失败 [%s]: %w", query, err)
|
||
}
|
||
}
|
||
|
||
// 为现有数据库添加新字段(向后兼容)
|
||
alterQueries := []string{
|
||
`ALTER TABLE exchanges ADD COLUMN hyperliquid_wallet_addr TEXT DEFAULT ''`,
|
||
`ALTER TABLE exchanges ADD COLUMN aster_user TEXT DEFAULT ''`,
|
||
`ALTER TABLE exchanges ADD COLUMN aster_signer TEXT DEFAULT ''`,
|
||
`ALTER TABLE exchanges ADD COLUMN aster_private_key TEXT DEFAULT ''`,
|
||
`ALTER TABLE exchanges ADD COLUMN lighter_wallet_addr TEXT DEFAULT ''`,
|
||
`ALTER TABLE exchanges ADD COLUMN lighter_private_key TEXT DEFAULT ''`,
|
||
`ALTER TABLE exchanges ADD COLUMN lighter_api_key_private_key TEXT DEFAULT ''`,
|
||
`ALTER TABLE traders ADD COLUMN custom_prompt TEXT DEFAULT ''`,
|
||
`ALTER TABLE traders ADD COLUMN override_base_prompt BOOLEAN DEFAULT 0`,
|
||
`ALTER TABLE traders ADD COLUMN is_cross_margin BOOLEAN DEFAULT 1`, // 默认为全仓模式
|
||
`ALTER TABLE traders ADD COLUMN use_default_coins BOOLEAN DEFAULT 1`, // 默认使用默认币种
|
||
`ALTER TABLE traders ADD COLUMN custom_coins TEXT DEFAULT ''`, // 自定义币种列表(JSON格式)
|
||
`ALTER TABLE traders ADD COLUMN btc_eth_leverage INTEGER DEFAULT 5`, // BTC/ETH杠杆倍数
|
||
`ALTER TABLE traders ADD COLUMN altcoin_leverage INTEGER DEFAULT 5`, // 山寨币杠杆倍数
|
||
`ALTER TABLE traders ADD COLUMN trading_symbols TEXT DEFAULT ''`, // 交易币种,逗号分隔
|
||
`ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`, // 是否使用COIN POOL信号源
|
||
`ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`, // 是否使用OI TOP信号源
|
||
`ALTER TABLE traders ADD COLUMN system_prompt_template TEXT DEFAULT 'default'`, // 系统提示词模板名称
|
||
`ALTER TABLE ai_models ADD COLUMN custom_api_url TEXT DEFAULT ''`, // 自定义API地址
|
||
`ALTER TABLE ai_models ADD COLUMN custom_model_name TEXT DEFAULT ''`, // 自定义模型名称
|
||
}
|
||
|
||
for _, query := range alterQueries {
|
||
// 忽略已存在字段的错误
|
||
d.db.Exec(query)
|
||
}
|
||
|
||
// 检查是否需要迁移exchanges表的主键结构
|
||
err := d.migrateExchangesTable()
|
||
if err != nil {
|
||
log.Printf("⚠️ 迁移exchanges表失败: %v", err)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// initDefaultData 初始化默认数据
|
||
func (d *Database) initDefaultData() error {
|
||
// 初始化AI模型(使用default用户)
|
||
aiModels := []struct {
|
||
id, name, provider string
|
||
}{
|
||
{"deepseek", "DeepSeek", "deepseek"},
|
||
{"qwen", "Qwen", "qwen"},
|
||
}
|
||
|
||
for _, model := range aiModels {
|
||
_, err := d.db.Exec(`
|
||
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled)
|
||
VALUES (?, 'default', ?, ?, 0)
|
||
`, model.id, model.name, model.provider)
|
||
if err != nil {
|
||
return fmt.Errorf("初始化AI模型失败: %w", err)
|
||
}
|
||
}
|
||
|
||
// 初始化交易所(使用default用户)
|
||
exchanges := []struct {
|
||
id, name, typ string
|
||
}{
|
||
{"binance", "Binance Futures", "binance"},
|
||
{"hyperliquid", "Hyperliquid", "hyperliquid"},
|
||
{"aster", "Aster DEX", "aster"},
|
||
{"lighter", "LIGHTER DEX", "lighter"},
|
||
}
|
||
|
||
for _, exchange := range exchanges {
|
||
_, err := d.db.Exec(`
|
||
INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled)
|
||
VALUES (?, 'default', ?, ?, 0)
|
||
`, exchange.id, exchange.name, exchange.typ)
|
||
if err != nil {
|
||
return fmt.Errorf("初始化交易所失败: %w", err)
|
||
}
|
||
}
|
||
|
||
// 初始化系统配置 - 创建所有字段,设置默认值,后续由config.json同步更新
|
||
systemConfigs := map[string]string{
|
||
"beta_mode": "false", // 默认关闭内测模式
|
||
"api_server_port": "8080", // 默认API端口
|
||
"use_default_coins": "true", // 默认使用内置币种列表
|
||
"default_coins": `["BTCUSDT","ETHUSDT","SOLUSDT","BNBUSDT","XRPUSDT","DOGEUSDT","ADAUSDT","HYPEUSDT"]`, // 默认币种列表(JSON格式)
|
||
"max_daily_loss": "10.0", // 最大日损失百分比
|
||
"max_drawdown": "20.0", // 最大回撤百分比
|
||
"stop_trading_minutes": "60", // 停止交易时间(分钟)
|
||
"btc_eth_leverage": "5", // BTC/ETH杠杆倍数
|
||
"altcoin_leverage": "5", // 山寨币杠杆倍数
|
||
"jwt_secret": "", // JWT密钥,默认为空,由config.json或系统生成
|
||
"registration_enabled": "true", // 默认允许注册
|
||
}
|
||
|
||
for key, value := range systemConfigs {
|
||
_, err := d.db.Exec(`
|
||
INSERT OR IGNORE INTO system_config (key, value)
|
||
VALUES (?, ?)
|
||
`, key, value)
|
||
if err != nil {
|
||
return fmt.Errorf("初始化系统配置失败: %w", err)
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// migrateExchangesTable 迁移exchanges表支持多用户
|
||
func (d *Database) migrateExchangesTable() error {
|
||
// 检查是否已经迁移过
|
||
var count int
|
||
err := d.db.QueryRow(`
|
||
SELECT COUNT(*) FROM sqlite_master
|
||
WHERE type='table' AND name='exchanges_new'
|
||
`).Scan(&count)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 如果已经迁移过,直接返回
|
||
if count > 0 {
|
||
return nil
|
||
}
|
||
|
||
log.Printf("🔄 开始迁移exchanges表...")
|
||
|
||
// 创建新的exchanges表,使用复合主键
|
||
_, err = d.db.Exec(`
|
||
CREATE TABLE exchanges_new (
|
||
id TEXT NOT NULL,
|
||
user_id TEXT NOT NULL DEFAULT 'default',
|
||
name TEXT NOT NULL,
|
||
type TEXT NOT NULL,
|
||
enabled BOOLEAN DEFAULT 0,
|
||
api_key TEXT DEFAULT '',
|
||
secret_key TEXT DEFAULT '',
|
||
testnet BOOLEAN DEFAULT 0,
|
||
hyperliquid_wallet_addr TEXT DEFAULT '',
|
||
aster_user TEXT DEFAULT '',
|
||
aster_signer TEXT DEFAULT '',
|
||
aster_private_key TEXT DEFAULT '',
|
||
lighter_wallet_addr TEXT DEFAULT '',
|
||
lighter_private_key TEXT DEFAULT '',
|
||
lighter_api_key_private_key TEXT DEFAULT '',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
PRIMARY KEY (id, user_id),
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||
)
|
||
`)
|
||
if err != nil {
|
||
return fmt.Errorf("创建新exchanges表失败: %w", err)
|
||
}
|
||
|
||
// 复制数据到新表
|
||
_, err = d.db.Exec(`
|
||
INSERT INTO exchanges_new
|
||
SELECT * FROM exchanges
|
||
`)
|
||
if err != nil {
|
||
return fmt.Errorf("复制数据失败: %w", err)
|
||
}
|
||
|
||
// 删除旧表
|
||
_, err = d.db.Exec(`DROP TABLE exchanges`)
|
||
if err != nil {
|
||
return fmt.Errorf("删除旧表失败: %w", err)
|
||
}
|
||
|
||
// 重命名新表
|
||
_, err = d.db.Exec(`ALTER TABLE exchanges_new RENAME TO exchanges`)
|
||
if err != nil {
|
||
return fmt.Errorf("重命名表失败: %w", err)
|
||
}
|
||
|
||
// 重新创建触发器
|
||
_, err = d.db.Exec(`
|
||
CREATE TRIGGER IF NOT EXISTS update_exchanges_updated_at
|
||
AFTER UPDATE ON exchanges
|
||
BEGIN
|
||
UPDATE exchanges SET updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = NEW.id AND user_id = NEW.user_id;
|
||
END
|
||
`)
|
||
if err != nil {
|
||
return fmt.Errorf("创建触发器失败: %w", err)
|
||
}
|
||
|
||
log.Printf("✅ exchanges表迁移完成")
|
||
return nil
|
||
}
|
||
|
||
// User 用户配置
|
||
type User struct {
|
||
ID string `json:"id"`
|
||
Email string `json:"email"`
|
||
PasswordHash string `json:"-"` // 不返回到前端
|
||
OTPSecret string `json:"-"` // 不返回到前端
|
||
OTPVerified bool `json:"otp_verified"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// AIModelConfig AI模型配置
|
||
type AIModelConfig struct {
|
||
ID string `json:"id"`
|
||
UserID string `json:"user_id"`
|
||
Name string `json:"name"`
|
||
Provider string `json:"provider"`
|
||
Enabled bool `json:"enabled"`
|
||
APIKey string `json:"apiKey"`
|
||
CustomAPIURL string `json:"customApiUrl"`
|
||
CustomModelName string `json:"customModelName"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// ExchangeConfig 交易所配置
|
||
type ExchangeConfig struct {
|
||
ID string `json:"id"`
|
||
UserID string `json:"user_id"`
|
||
Name string `json:"name"`
|
||
Type string `json:"type"`
|
||
Enabled bool `json:"enabled"`
|
||
APIKey string `json:"apiKey"` // For Binance: API Key; For Hyperliquid: Agent Private Key (should have ~0 balance)
|
||
SecretKey string `json:"secretKey"` // For Binance: Secret Key; Not used for Hyperliquid
|
||
Testnet bool `json:"testnet"`
|
||
// Hyperliquid Agent Wallet configuration (following official best practices)
|
||
// Reference: https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets
|
||
HyperliquidWalletAddr string `json:"hyperliquidWalletAddr"` // Main Wallet Address (holds funds, never expose private key)
|
||
// Aster 特定字段
|
||
AsterUser string `json:"asterUser"`
|
||
AsterSigner string `json:"asterSigner"`
|
||
AsterPrivateKey string `json:"asterPrivateKey"`
|
||
// LIGHTER 特定字段
|
||
LighterWalletAddr string `json:"lighterWalletAddr"` // Ethereum 钱包地址 (L1)
|
||
LighterPrivateKey string `json:"lighterPrivateKey"` // L1私钥(用于识别账户)
|
||
LighterAPIKeyPrivateKey string `json:"lighterAPIKeyPrivateKey"` // API Key私钥(40字节,用于签名交易)
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// TraderRecord 交易员配置(数据库实体)
|
||
type TraderRecord struct {
|
||
ID string `json:"id"`
|
||
UserID string `json:"user_id"`
|
||
Name string `json:"name"`
|
||
AIModelID string `json:"ai_model_id"`
|
||
ExchangeID string `json:"exchange_id"`
|
||
InitialBalance float64 `json:"initial_balance"`
|
||
ScanIntervalMinutes int `json:"scan_interval_minutes"`
|
||
IsRunning bool `json:"is_running"`
|
||
BTCETHLeverage int `json:"btc_eth_leverage"` // BTC/ETH杠杆倍数
|
||
AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币杠杆倍数
|
||
TradingSymbols string `json:"trading_symbols"` // 交易币种,逗号分隔
|
||
UseCoinPool bool `json:"use_coin_pool"` // 是否使用COIN POOL信号源
|
||
UseOITop bool `json:"use_oi_top"` // 是否使用OI TOP信号源
|
||
CustomPrompt string `json:"custom_prompt"` // 自定义交易策略prompt
|
||
OverrideBasePrompt bool `json:"override_base_prompt"` // 是否覆盖基础prompt
|
||
SystemPromptTemplate string `json:"system_prompt_template"` // 系统提示词模板名称
|
||
IsCrossMargin bool `json:"is_cross_margin"` // 是否为全仓模式(true=全仓,false=逐仓)
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// UserSignalSource 用户信号源配置
|
||
type UserSignalSource struct {
|
||
ID int `json:"id"`
|
||
UserID string `json:"user_id"`
|
||
CoinPoolURL string `json:"coin_pool_url"`
|
||
OITopURL string `json:"oi_top_url"`
|
||
CreatedAt time.Time `json:"created_at"`
|
||
UpdatedAt time.Time `json:"updated_at"`
|
||
}
|
||
|
||
// GenerateOTPSecret 生成OTP密钥
|
||
func GenerateOTPSecret() (string, error) {
|
||
secret := make([]byte, 20)
|
||
_, err := rand.Read(secret)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return base32.StdEncoding.EncodeToString(secret), nil
|
||
}
|
||
|
||
// CreateUser 创建用户
|
||
func (d *Database) CreateUser(user *User) error {
|
||
_, err := d.db.Exec(`
|
||
INSERT INTO users (id, email, password_hash, otp_secret, otp_verified)
|
||
VALUES (?, ?, ?, ?, ?)
|
||
`, user.ID, user.Email, user.PasswordHash, user.OTPSecret, user.OTPVerified)
|
||
return err
|
||
}
|
||
|
||
// EnsureAdminUser 确保admin用户存在(用于管理员模式)
|
||
func (d *Database) EnsureAdminUser() error {
|
||
// 检查admin用户是否已存在
|
||
var count int
|
||
err := d.db.QueryRow(`SELECT COUNT(*) FROM users WHERE id = 'admin'`).Scan(&count)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// 如果已存在,直接返回
|
||
if count > 0 {
|
||
return nil
|
||
}
|
||
|
||
// 创建admin用户(密码为空,因为管理员模式下不需要密码)
|
||
adminUser := &User{
|
||
ID: "admin",
|
||
Email: "admin@localhost",
|
||
PasswordHash: "", // 管理员模式下不使用密码
|
||
OTPSecret: "",
|
||
OTPVerified: true,
|
||
}
|
||
|
||
return d.CreateUser(adminUser)
|
||
}
|
||
|
||
// GetUserByEmail 通过邮箱获取用户
|
||
func (d *Database) GetUserByEmail(email string) (*User, error) {
|
||
var user User
|
||
err := d.db.QueryRow(`
|
||
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
|
||
FROM users WHERE email = ?
|
||
`, email).Scan(
|
||
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
|
||
&user.OTPVerified, &user.CreatedAt, &user.UpdatedAt,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &user, nil
|
||
}
|
||
|
||
// GetUserByID 通过ID获取用户
|
||
func (d *Database) GetUserByID(userID string) (*User, error) {
|
||
var user User
|
||
err := d.db.QueryRow(`
|
||
SELECT id, email, password_hash, otp_secret, otp_verified, created_at, updated_at
|
||
FROM users WHERE id = ?
|
||
`, userID).Scan(
|
||
&user.ID, &user.Email, &user.PasswordHash, &user.OTPSecret,
|
||
&user.OTPVerified, &user.CreatedAt, &user.UpdatedAt,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &user, nil
|
||
}
|
||
|
||
// GetAllUsers 获取所有用户ID列表
|
||
func (d *Database) GetAllUsers() ([]string, error) {
|
||
rows, err := d.db.Query(`SELECT id FROM users ORDER BY id`)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var userIDs []string
|
||
for rows.Next() {
|
||
var userID string
|
||
if err := rows.Scan(&userID); err != nil {
|
||
return nil, err
|
||
}
|
||
userIDs = append(userIDs, userID)
|
||
}
|
||
return userIDs, nil
|
||
}
|
||
|
||
// UpdateUserOTPVerified 更新用户OTP验证状态
|
||
func (d *Database) UpdateUserOTPVerified(userID string, verified bool) error {
|
||
_, err := d.db.Exec(`UPDATE users SET otp_verified = ? WHERE id = ?`, verified, userID)
|
||
return err
|
||
}
|
||
|
||
// UpdateUserPassword 更新用户密码
|
||
func (d *Database) UpdateUserPassword(userID, passwordHash string) error {
|
||
_, err := d.db.Exec(`
|
||
UPDATE users
|
||
SET password_hash = ?, updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ?
|
||
`, passwordHash, userID)
|
||
return err
|
||
}
|
||
|
||
// GetAIModels 获取用户的AI模型配置
|
||
func (d *Database) GetAIModels(userID string) ([]*AIModelConfig, error) {
|
||
rows, err := d.db.Query(`
|
||
SELECT id, user_id, name, provider, enabled, api_key,
|
||
COALESCE(custom_api_url, '') as custom_api_url,
|
||
COALESCE(custom_model_name, '') as custom_model_name,
|
||
created_at, updated_at
|
||
FROM ai_models WHERE user_id = ? ORDER BY id
|
||
`, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
// 初始化为空切片而不是nil,确保JSON序列化为[]而不是null
|
||
models := make([]*AIModelConfig, 0)
|
||
for rows.Next() {
|
||
var model AIModelConfig
|
||
err := rows.Scan(
|
||
&model.ID, &model.UserID, &model.Name, &model.Provider,
|
||
&model.Enabled, &model.APIKey, &model.CustomAPIURL, &model.CustomModelName,
|
||
&model.CreatedAt, &model.UpdatedAt,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// 解密API Key
|
||
model.APIKey = d.decryptSensitiveData(model.APIKey)
|
||
models = append(models, &model)
|
||
}
|
||
|
||
return models, nil
|
||
}
|
||
|
||
// UpdateAIModel 更新AI模型配置,如果不存在则创建用户特定配置
|
||
func (d *Database) UpdateAIModel(userID, id string, enabled bool, apiKey, customAPIURL, customModelName string) error {
|
||
// 先尝试精确匹配 ID(新版逻辑,支持多个相同 provider 的模型)
|
||
var existingID string
|
||
err := d.db.QueryRow(`
|
||
SELECT id FROM ai_models WHERE user_id = ? AND id = ? LIMIT 1
|
||
`, userID, id).Scan(&existingID)
|
||
|
||
if err == nil {
|
||
// 找到了现有配置(精确匹配 ID),更新它
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
_, err = d.db.Exec(`
|
||
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
||
WHERE id = ? AND user_id = ?
|
||
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
|
||
return err
|
||
}
|
||
|
||
// ID 不存在,尝试兼容旧逻辑:将 id 作为 provider 查找
|
||
provider := id
|
||
err = d.db.QueryRow(`
|
||
SELECT id FROM ai_models WHERE user_id = ? AND provider = ? LIMIT 1
|
||
`, userID, provider).Scan(&existingID)
|
||
|
||
if err == nil {
|
||
// 找到了现有配置(通过 provider 匹配,兼容旧版),更新它
|
||
log.Printf("⚠️ 使用旧版 provider 匹配更新模型: %s -> %s", provider, existingID)
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
_, err = d.db.Exec(`
|
||
UPDATE ai_models SET enabled = ?, api_key = ?, custom_api_url = ?, custom_model_name = ?, updated_at = datetime('now')
|
||
WHERE id = ? AND user_id = ?
|
||
`, enabled, encryptedAPIKey, customAPIURL, customModelName, existingID, userID)
|
||
return err
|
||
}
|
||
|
||
// 没有找到任何现有配置,创建新的
|
||
// 推断 provider(从 id 中提取,或者直接使用 id)
|
||
if provider == id && (provider == "deepseek" || provider == "qwen") {
|
||
// id 本身就是 provider
|
||
provider = id
|
||
} else {
|
||
// 从 id 中提取 provider(假设格式是 userID_provider 或 timestamp_userID_provider)
|
||
parts := strings.Split(id, "_")
|
||
if len(parts) >= 2 {
|
||
provider = parts[len(parts)-1] // 取最后一部分作为 provider
|
||
} else {
|
||
provider = id
|
||
}
|
||
}
|
||
|
||
// 获取模型的基本信息
|
||
var name string
|
||
err = d.db.QueryRow(`
|
||
SELECT name FROM ai_models WHERE provider = ? LIMIT 1
|
||
`, provider).Scan(&name)
|
||
if err != nil {
|
||
// 如果找不到基本信息,使用默认值
|
||
if provider == "deepseek" {
|
||
name = "DeepSeek AI"
|
||
} else if provider == "qwen" {
|
||
name = "Qwen AI"
|
||
} else {
|
||
name = provider + " AI"
|
||
}
|
||
}
|
||
|
||
// 如果传入的 ID 已经是完整格式(如 "admin_deepseek_custom1"),直接使用
|
||
// 否则生成新的 ID
|
||
newModelID := id
|
||
if id == provider {
|
||
// id 就是 provider,生成新的用户特定 ID
|
||
newModelID = fmt.Sprintf("%s_%s", userID, provider)
|
||
}
|
||
|
||
log.Printf("✓ 创建新的 AI 模型配置: ID=%s, Provider=%s, Name=%s", newModelID, provider, name)
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
_, err = d.db.Exec(`
|
||
INSERT INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url, custom_model_name, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||
`, newModelID, userID, name, provider, enabled, encryptedAPIKey, customAPIURL, customModelName)
|
||
|
||
return err
|
||
}
|
||
|
||
// GetExchanges 获取用户的交易所配置
|
||
func (d *Database) GetExchanges(userID string) ([]*ExchangeConfig, error) {
|
||
rows, err := d.db.Query(`
|
||
SELECT id, user_id, name, type, enabled, api_key, secret_key, testnet,
|
||
COALESCE(hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
|
||
COALESCE(aster_user, '') as aster_user,
|
||
COALESCE(aster_signer, '') as aster_signer,
|
||
COALESCE(aster_private_key, '') as aster_private_key,
|
||
COALESCE(lighter_wallet_addr, '') as lighter_wallet_addr,
|
||
COALESCE(lighter_private_key, '') as lighter_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.LighterWalletAddr, &exchange.LighterPrivateKey,
|
||
&exchange.CreatedAt, &exchange.UpdatedAt,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 解密敏感字段
|
||
exchange.APIKey = d.decryptSensitiveData(exchange.APIKey)
|
||
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
|
||
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
|
||
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
|
||
|
||
exchanges = append(exchanges, &exchange)
|
||
}
|
||
|
||
return exchanges, nil
|
||
}
|
||
|
||
// UpdateExchange 更新交易所配置,如果不存在则创建用户特定配置
|
||
// 🔒 安全特性:空值不会覆盖现有的敏感字段(api_key, secret_key, aster_private_key, lighter_private_key)
|
||
func (d *Database) UpdateExchange(userID, id string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, lighterWalletAddr, lighterPrivateKey string) error {
|
||
log.Printf("🔧 UpdateExchange: userID=%s, id=%s, enabled=%v", userID, id, enabled)
|
||
|
||
// 构建动态 UPDATE SET 子句
|
||
// 基础字段:总是更新
|
||
setClauses := []string{
|
||
"enabled = ?",
|
||
"testnet = ?",
|
||
"hyperliquid_wallet_addr = ?",
|
||
"aster_user = ?",
|
||
"aster_signer = ?",
|
||
"lighter_wallet_addr = ?",
|
||
"updated_at = datetime('now')",
|
||
}
|
||
args := []interface{}{enabled, testnet, hyperliquidWalletAddr, asterUser, asterSigner, lighterWalletAddr}
|
||
|
||
// 🔒 敏感字段:只在非空时更新(保护现有数据)
|
||
if apiKey != "" {
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
setClauses = append(setClauses, "api_key = ?")
|
||
args = append(args, encryptedAPIKey)
|
||
}
|
||
|
||
if secretKey != "" {
|
||
encryptedSecretKey := d.encryptSensitiveData(secretKey)
|
||
setClauses = append(setClauses, "secret_key = ?")
|
||
args = append(args, encryptedSecretKey)
|
||
}
|
||
|
||
if asterPrivateKey != "" {
|
||
encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey)
|
||
setClauses = append(setClauses, "aster_private_key = ?")
|
||
args = append(args, encryptedAsterPrivateKey)
|
||
}
|
||
|
||
if lighterPrivateKey != "" {
|
||
encryptedLighterPrivateKey := d.encryptSensitiveData(lighterPrivateKey)
|
||
setClauses = append(setClauses, "lighter_private_key = ?")
|
||
args = append(args, encryptedLighterPrivateKey)
|
||
}
|
||
|
||
// WHERE 条件
|
||
args = append(args, id, userID)
|
||
|
||
// 构建完整的 UPDATE 语句
|
||
query := fmt.Sprintf(`
|
||
UPDATE exchanges SET %s
|
||
WHERE id = ? AND user_id = ?
|
||
`, strings.Join(setClauses, ", "))
|
||
|
||
// 执行更新
|
||
result, err := d.db.Exec(query, args...)
|
||
if err != nil {
|
||
log.Printf("❌ UpdateExchange: 更新失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
// 检查是否有行被更新
|
||
rowsAffected, err := result.RowsAffected()
|
||
if err != nil {
|
||
log.Printf("❌ UpdateExchange: 获取影响行数失败: %v", err)
|
||
return err
|
||
}
|
||
|
||
log.Printf("📊 UpdateExchange: 影响行数 = %d", rowsAffected)
|
||
|
||
// 如果没有行被更新,说明用户没有这个交易所的配置,需要创建
|
||
if rowsAffected == 0 {
|
||
log.Printf("💡 UpdateExchange: 没有现有记录,创建新记录")
|
||
|
||
// 根据交易所ID确定基本信息
|
||
var name, typ string
|
||
if id == "binance" {
|
||
name = "Binance Futures"
|
||
typ = "cex"
|
||
} else if id == "hyperliquid" {
|
||
name = "Hyperliquid"
|
||
typ = "dex"
|
||
} else if id == "aster" {
|
||
name = "Aster DEX"
|
||
typ = "dex"
|
||
} else if id == "lighter" {
|
||
name = "LIGHTER DEX"
|
||
typ = "dex"
|
||
} else {
|
||
name = id + " Exchange"
|
||
typ = "cex"
|
||
}
|
||
|
||
log.Printf("🆕 UpdateExchange: 创建新记录 ID=%s, name=%s, type=%s", id, name, typ)
|
||
|
||
// 加密敏感字段
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
encryptedSecretKey := d.encryptSensitiveData(secretKey)
|
||
encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey)
|
||
encryptedLighterPrivateKey := d.encryptSensitiveData(lighterPrivateKey)
|
||
|
||
// 创建用户特定的配置,使用原始的交易所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,
|
||
lighter_wallet_addr, lighter_private_key, created_at, updated_at)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||
`, id, userID, name, typ, enabled, encryptedAPIKey, encryptedSecretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, encryptedAsterPrivateKey, lighterWalletAddr, encryptedLighterPrivateKey)
|
||
|
||
if err != nil {
|
||
log.Printf("❌ UpdateExchange: 创建记录失败: %v", err)
|
||
} else {
|
||
log.Printf("✅ UpdateExchange: 创建记录成功")
|
||
}
|
||
return err
|
||
}
|
||
|
||
log.Printf("✅ UpdateExchange: 更新现有记录成功")
|
||
return nil
|
||
}
|
||
|
||
// CreateAIModel 创建AI模型配置
|
||
func (d *Database) CreateAIModel(userID, id, name, provider string, enabled bool, apiKey, customAPIURL string) error {
|
||
_, err := d.db.Exec(`
|
||
INSERT OR IGNORE INTO ai_models (id, user_id, name, provider, enabled, api_key, custom_api_url)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||
`, id, userID, name, provider, enabled, apiKey, customAPIURL)
|
||
return err
|
||
}
|
||
|
||
// CreateExchange 创建交易所配置
|
||
func (d *Database) CreateExchange(userID, id, name, typ string, enabled bool, apiKey, secretKey string, testnet bool, hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error {
|
||
// 加密敏感字段
|
||
encryptedAPIKey := d.encryptSensitiveData(apiKey)
|
||
encryptedSecretKey := d.encryptSensitiveData(secretKey)
|
||
encryptedAsterPrivateKey := d.encryptSensitiveData(asterPrivateKey)
|
||
|
||
_, err := d.db.Exec(`
|
||
INSERT OR IGNORE INTO exchanges (id, user_id, name, type, enabled, api_key, secret_key, testnet, hyperliquid_wallet_addr, aster_user, aster_signer, aster_private_key, lighter_wallet_addr, lighter_private_key)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '')
|
||
`, id, userID, name, typ, enabled, encryptedAPIKey, encryptedSecretKey, testnet, hyperliquidWalletAddr, asterUser, asterSigner, encryptedAsterPrivateKey)
|
||
return err
|
||
}
|
||
|
||
// CreateTrader 创建交易员
|
||
func (d *Database) CreateTrader(trader *TraderRecord) error {
|
||
_, err := d.db.Exec(`
|
||
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool, use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin)
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin)
|
||
return err
|
||
}
|
||
|
||
// GetTraders 获取用户的交易员
|
||
func (d *Database) GetTraders(userID string) ([]*TraderRecord, error) {
|
||
rows, err := d.db.Query(`
|
||
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
|
||
COALESCE(btc_eth_leverage, 5) as btc_eth_leverage, COALESCE(altcoin_leverage, 5) as altcoin_leverage,
|
||
COALESCE(trading_symbols, '') as trading_symbols,
|
||
COALESCE(use_coin_pool, 0) as use_coin_pool, COALESCE(use_oi_top, 0) as use_oi_top,
|
||
COALESCE(custom_prompt, '') as custom_prompt, COALESCE(override_base_prompt, 0) as override_base_prompt,
|
||
COALESCE(system_prompt_template, 'default') as system_prompt_template,
|
||
COALESCE(is_cross_margin, 1) as is_cross_margin, created_at, updated_at
|
||
FROM traders WHERE user_id = ? ORDER BY created_at DESC
|
||
`, userID)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer rows.Close()
|
||
|
||
var traders []*TraderRecord
|
||
for rows.Next() {
|
||
var trader TraderRecord
|
||
err := rows.Scan(
|
||
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
||
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
|
||
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols,
|
||
&trader.UseCoinPool, &trader.UseOITop,
|
||
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
|
||
&trader.IsCrossMargin,
|
||
&trader.CreatedAt, &trader.UpdatedAt,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
traders = append(traders, &trader)
|
||
}
|
||
|
||
return traders, nil
|
||
}
|
||
|
||
// UpdateTraderStatus 更新交易员状态
|
||
func (d *Database) UpdateTraderStatus(userID, id string, isRunning bool) error {
|
||
_, err := d.db.Exec(`UPDATE traders SET is_running = ? WHERE id = ? AND user_id = ?`, isRunning, id, userID)
|
||
return err
|
||
}
|
||
|
||
// UpdateTrader 更新交易员配置
|
||
func (d *Database) UpdateTrader(trader *TraderRecord) error {
|
||
_, err := d.db.Exec(`
|
||
UPDATE traders SET
|
||
name = ?, ai_model_id = ?, exchange_id = ?,
|
||
scan_interval_minutes = ?, btc_eth_leverage = ?, altcoin_leverage = ?,
|
||
trading_symbols = ?, custom_prompt = ?, override_base_prompt = ?,
|
||
system_prompt_template = ?, is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP
|
||
WHERE id = ? AND user_id = ?
|
||
`, trader.Name, trader.AIModelID, trader.ExchangeID,
|
||
trader.ScanIntervalMinutes, trader.BTCETHLeverage, trader.AltcoinLeverage,
|
||
trader.TradingSymbols, trader.CustomPrompt, trader.OverrideBasePrompt,
|
||
trader.SystemPromptTemplate, trader.IsCrossMargin, trader.ID, trader.UserID)
|
||
return err
|
||
}
|
||
|
||
// UpdateTraderCustomPrompt 更新交易员自定义Prompt
|
||
func (d *Database) UpdateTraderCustomPrompt(userID, id string, customPrompt string, overrideBase bool) error {
|
||
_, err := d.db.Exec(`UPDATE traders SET custom_prompt = ?, override_base_prompt = ? WHERE id = ? AND user_id = ?`, customPrompt, overrideBase, id, userID)
|
||
return err
|
||
}
|
||
|
||
// UpdateTraderInitialBalance 更新交易员初始余额(仅支持手动更新)
|
||
// ⚠️ 注意:系统不会自动调用此方法,仅供用户在充值/提现后手动同步使用
|
||
func (d *Database) UpdateTraderInitialBalance(userID, id string, newBalance float64) error {
|
||
_, err := d.db.Exec(`UPDATE traders SET initial_balance = ? WHERE id = ? AND user_id = ?`, newBalance, id, userID)
|
||
return err
|
||
}
|
||
|
||
// DeleteTrader 删除交易员
|
||
func (d *Database) DeleteTrader(userID, id string) error {
|
||
_, err := d.db.Exec(`DELETE FROM traders WHERE id = ? AND user_id = ?`, id, userID)
|
||
return err
|
||
}
|
||
|
||
// GetTraderConfig 获取交易员完整配置(包含AI模型和交易所信息)
|
||
func (d *Database) GetTraderConfig(userID, traderID string) (*TraderRecord, *AIModelConfig, *ExchangeConfig, error) {
|
||
var trader TraderRecord
|
||
var aiModel AIModelConfig
|
||
var exchange ExchangeConfig
|
||
|
||
err := d.db.QueryRow(`
|
||
SELECT
|
||
t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running,
|
||
COALESCE(t.btc_eth_leverage, 5) as btc_eth_leverage,
|
||
COALESCE(t.altcoin_leverage, 5) as altcoin_leverage,
|
||
COALESCE(t.trading_symbols, '') as trading_symbols,
|
||
COALESCE(t.use_coin_pool, 0) as use_coin_pool,
|
||
COALESCE(t.use_oi_top, 0) as use_oi_top,
|
||
COALESCE(t.custom_prompt, '') as custom_prompt,
|
||
COALESCE(t.override_base_prompt, 0) as override_base_prompt,
|
||
COALESCE(t.system_prompt_template, 'default') as system_prompt_template,
|
||
COALESCE(t.is_cross_margin, 1) as is_cross_margin,
|
||
t.created_at, t.updated_at,
|
||
a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key,
|
||
COALESCE(a.custom_api_url, '') as custom_api_url,
|
||
COALESCE(a.custom_model_name, '') as custom_model_name,
|
||
a.created_at, a.updated_at,
|
||
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet,
|
||
COALESCE(e.hyperliquid_wallet_addr, '') as hyperliquid_wallet_addr,
|
||
COALESCE(e.aster_user, '') as aster_user,
|
||
COALESCE(e.aster_signer, '') as aster_signer,
|
||
COALESCE(e.aster_private_key, '') as aster_private_key,
|
||
COALESCE(e.lighter_wallet_addr, '') as lighter_wallet_addr,
|
||
COALESCE(e.lighter_private_key, '') as lighter_private_key,
|
||
e.created_at, e.updated_at
|
||
FROM traders t
|
||
JOIN ai_models a ON t.ai_model_id = a.id AND t.user_id = a.user_id
|
||
JOIN exchanges e ON t.exchange_id = e.id AND t.user_id = e.user_id
|
||
WHERE t.id = ? AND t.user_id = ?
|
||
`, traderID, userID).Scan(
|
||
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
|
||
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
|
||
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols,
|
||
&trader.UseCoinPool, &trader.UseOITop,
|
||
&trader.CustomPrompt, &trader.OverrideBasePrompt, &trader.SystemPromptTemplate,
|
||
&trader.IsCrossMargin,
|
||
&trader.CreatedAt, &trader.UpdatedAt,
|
||
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
|
||
&aiModel.CustomAPIURL, &aiModel.CustomModelName,
|
||
&aiModel.CreatedAt, &aiModel.UpdatedAt,
|
||
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled,
|
||
&exchange.APIKey, &exchange.SecretKey, &exchange.Testnet,
|
||
&exchange.HyperliquidWalletAddr, &exchange.AsterUser, &exchange.AsterSigner, &exchange.AsterPrivateKey,
|
||
&exchange.LighterWalletAddr, &exchange.LighterPrivateKey,
|
||
&exchange.CreatedAt, &exchange.UpdatedAt,
|
||
)
|
||
|
||
if err != nil {
|
||
return nil, nil, nil, err
|
||
}
|
||
|
||
// 解密敏感数据
|
||
aiModel.APIKey = d.decryptSensitiveData(aiModel.APIKey)
|
||
exchange.APIKey = d.decryptSensitiveData(exchange.APIKey)
|
||
exchange.SecretKey = d.decryptSensitiveData(exchange.SecretKey)
|
||
exchange.AsterPrivateKey = d.decryptSensitiveData(exchange.AsterPrivateKey)
|
||
exchange.LighterPrivateKey = d.decryptSensitiveData(exchange.LighterPrivateKey)
|
||
|
||
return &trader, &aiModel, &exchange, nil
|
||
}
|
||
|
||
// GetSystemConfig 获取系统配置
|
||
func (d *Database) GetSystemConfig(key string) (string, error) {
|
||
var value string
|
||
err := d.db.QueryRow(`SELECT value FROM system_config WHERE key = ?`, key).Scan(&value)
|
||
return value, err
|
||
}
|
||
|
||
// SetSystemConfig 设置系统配置
|
||
func (d *Database) SetSystemConfig(key, value string) error {
|
||
_, err := d.db.Exec(`
|
||
INSERT OR REPLACE INTO system_config (key, value) VALUES (?, ?)
|
||
`, key, value)
|
||
return err
|
||
}
|
||
|
||
// CreateUserSignalSource 创建用户信号源配置
|
||
func (d *Database) CreateUserSignalSource(userID, coinPoolURL, oiTopURL string) error {
|
||
_, err := d.db.Exec(`
|
||
INSERT OR REPLACE INTO user_signal_sources (user_id, coin_pool_url, oi_top_url, updated_at)
|
||
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
||
`, userID, coinPoolURL, oiTopURL)
|
||
return err
|
||
}
|
||
|
||
// GetUserSignalSource 获取用户信号源配置
|
||
func (d *Database) GetUserSignalSource(userID string) (*UserSignalSource, error) {
|
||
var source UserSignalSource
|
||
err := d.db.QueryRow(`
|
||
SELECT id, user_id, coin_pool_url, oi_top_url, created_at, updated_at
|
||
FROM user_signal_sources WHERE user_id = ?
|
||
`, userID).Scan(
|
||
&source.ID, &source.UserID, &source.CoinPoolURL, &source.OITopURL,
|
||
&source.CreatedAt, &source.UpdatedAt,
|
||
)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return &source, nil
|
||
}
|
||
|
||
// UpdateUserSignalSource 更新用户信号源配置
|
||
func (d *Database) UpdateUserSignalSource(userID, coinPoolURL, oiTopURL string) error {
|
||
_, err := d.db.Exec(`
|
||
UPDATE user_signal_sources SET coin_pool_url = ?, oi_top_url = ?, updated_at = CURRENT_TIMESTAMP
|
||
WHERE user_id = ?
|
||
`, coinPoolURL, oiTopURL, userID)
|
||
return err
|
||
}
|
||
|
||
// GetCustomCoins 获取所有交易员自定义币种 / Get all trader-customized currencies
|
||
func (d *Database) GetCustomCoins() []string {
|
||
var symbol string
|
||
var symbols []string
|
||
_ = d.db.QueryRow(`
|
||
SELECT GROUP_CONCAT(custom_coins , ',') as symbol
|
||
FROM main.traders where custom_coins != ''
|
||
`).Scan(&symbol)
|
||
// 检测用户是否未配置币种 - 兼容性
|
||
if symbol == "" {
|
||
symbolJSON, _ := d.GetSystemConfig("default_coins")
|
||
if err := json.Unmarshal([]byte(symbolJSON), &symbols); err != nil {
|
||
log.Printf("⚠️ 解析default_coins配置失败: %v,使用硬编码默认值", err)
|
||
symbols = []string{"BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT"}
|
||
}
|
||
}
|
||
// filter Symbol
|
||
for _, s := range strings.Split(symbol, ",") {
|
||
if s == "" {
|
||
continue
|
||
}
|
||
coin := market.Normalize(s)
|
||
if !slices.Contains(symbols, coin) {
|
||
symbols = append(symbols, coin)
|
||
}
|
||
}
|
||
return symbols
|
||
}
|
||
|
||
// Close 关闭数据库连接
|
||
func (d *Database) Close() error {
|
||
return d.db.Close()
|
||
}
|
||
|
||
// LoadBetaCodesFromFile 从文件加载内测码到数据库
|
||
func (d *Database) LoadBetaCodesFromFile(filePath string) error {
|
||
// 读取文件内容
|
||
content, err := os.ReadFile(filePath)
|
||
if err != nil {
|
||
return fmt.Errorf("读取内测码文件失败: %w", err)
|
||
}
|
||
|
||
// 按行分割内测码
|
||
lines := strings.Split(string(content), "\n")
|
||
var codes []string
|
||
for _, line := range lines {
|
||
code := strings.TrimSpace(line)
|
||
if code != "" && !strings.HasPrefix(code, "#") {
|
||
codes = append(codes, code)
|
||
}
|
||
}
|
||
|
||
// 批量插入内测码
|
||
tx, err := d.db.Begin()
|
||
if err != nil {
|
||
return fmt.Errorf("开始事务失败: %w", err)
|
||
}
|
||
defer tx.Rollback()
|
||
|
||
stmt, err := tx.Prepare(`INSERT OR IGNORE INTO beta_codes (code) VALUES (?)`)
|
||
if err != nil {
|
||
return fmt.Errorf("准备语句失败: %w", err)
|
||
}
|
||
defer stmt.Close()
|
||
|
||
insertedCount := 0
|
||
for _, code := range codes {
|
||
result, err := stmt.Exec(code)
|
||
if err != nil {
|
||
log.Printf("插入内测码 %s 失败: %v", code, err)
|
||
continue
|
||
}
|
||
|
||
if rowsAffected, _ := result.RowsAffected(); rowsAffected > 0 {
|
||
insertedCount++
|
||
}
|
||
}
|
||
|
||
if err := tx.Commit(); err != nil {
|
||
return fmt.Errorf("提交事务失败: %w", err)
|
||
}
|
||
|
||
log.Printf("✅ 成功加载 %d 个内测码到数据库 (总计 %d 个)", insertedCount, len(codes))
|
||
return nil
|
||
}
|
||
|
||
// ValidateBetaCode 验证内测码是否有效且未使用
|
||
func (d *Database) ValidateBetaCode(code string) (bool, error) {
|
||
var used bool
|
||
err := d.db.QueryRow(`SELECT used FROM beta_codes WHERE code = ?`, code).Scan(&used)
|
||
if err != nil {
|
||
if err == sql.ErrNoRows {
|
||
return false, nil // 内测码不存在
|
||
}
|
||
return false, err
|
||
}
|
||
return !used, nil // 内测码存在且未使用
|
||
}
|
||
|
||
// UseBetaCode 使用内测码(标记为已使用)
|
||
func (d *Database) UseBetaCode(code, userEmail string) error {
|
||
result, err := d.db.Exec(`
|
||
UPDATE beta_codes SET used = 1, used_by = ?, used_at = CURRENT_TIMESTAMP
|
||
WHERE code = ? AND used = 0
|
||
`, userEmail, code)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
rowsAffected, err := result.RowsAffected()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if rowsAffected == 0 {
|
||
return fmt.Errorf("内测码无效或已被使用")
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetBetaCodeStats 获取内测码统计信息
|
||
func (d *Database) GetBetaCodeStats() (total, used int, err error) {
|
||
err = d.db.QueryRow(`SELECT COUNT(*) FROM beta_codes`).Scan(&total)
|
||
if err != nil {
|
||
return 0, 0, err
|
||
}
|
||
|
||
err = d.db.QueryRow(`SELECT COUNT(*) FROM beta_codes WHERE used = 1`).Scan(&used)
|
||
if err != nil {
|
||
return 0, 0, err
|
||
}
|
||
|
||
return total, used, nil
|
||
}
|
||
|
||
// SetCryptoService 设置加密服务
|
||
func (d *Database) SetCryptoService(cs *crypto.CryptoService) {
|
||
d.cryptoService = cs
|
||
}
|
||
|
||
// encryptSensitiveData 加密敏感数据用于存储
|
||
func (d *Database) encryptSensitiveData(plaintext string) string {
|
||
if d.cryptoService == nil || plaintext == "" {
|
||
return plaintext
|
||
}
|
||
|
||
encrypted, err := d.cryptoService.EncryptForStorage(plaintext)
|
||
if err != nil {
|
||
log.Printf("⚠️ 加密失败: %v", err)
|
||
return plaintext // 返回明文作为降级处理
|
||
}
|
||
|
||
return encrypted
|
||
}
|
||
|
||
// decryptSensitiveData 解密敏感数据
|
||
func (d *Database) decryptSensitiveData(encrypted string) string {
|
||
if d.cryptoService == nil || encrypted == "" {
|
||
return encrypted
|
||
}
|
||
|
||
// 如果不是加密格式,直接返回
|
||
if !d.cryptoService.IsEncryptedStorageValue(encrypted) {
|
||
return encrypted
|
||
}
|
||
|
||
decrypted, err := d.cryptoService.DecryptFromStorage(encrypted)
|
||
if err != nil {
|
||
log.Printf("⚠️ 解密失败: %v", err)
|
||
return encrypted // 返回加密文本作为降级处理
|
||
}
|
||
|
||
return decrypted
|
||
}
|