mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-17 01:14:40 +08:00
Refactor/trading actions (#1169)
* refactor: 简化交易动作,移除 update_stop_loss/update_take_profit/partial_close - 移除 Decision 结构体中的 NewStopLoss, NewTakeProfit, ClosePercentage 字段 - 删除 executeUpdateStopLossWithRecord, executeUpdateTakeProfitWithRecord, executePartialCloseWithRecord 函数 - 简化 logger 中的 partial_close 聚合逻辑 - 更新 AI prompt 和验证逻辑,只保留 6 个核心动作 - 清理相关测试代码 保留的交易动作: open_long, open_short, close_long, close_short, hold, wait * refactor: 移除 AI学习与反思 模块 - 删除前端 AILearning.tsx 组件和相关引用 - 删除后端 /performance API 接口 - 删除 logger 中 AnalyzePerformance、calculateSharpeRatio 等函数 - 删除 PerformanceAnalysis、TradeOutcome、SymbolPerformance 等结构体 - 删除 Context 中的 Performance 字段 - 移除 AI prompt 中夏普比率自我进化相关内容 - 清理 i18n 翻译文件中的相关条目 该模块基于磁盘存储计算,经常出错,做减法移除 * refactor: 将数据库操作统一迁移到 store 包 - 新增 store/ 包,统一管理所有数据库操作 - store.go: 主 Store 结构,懒加载各子模块 - user.go, ai_model.go, exchange.go, trader.go 等子模块 - 支持加密/解密函数注入 (SetCryptoFuncs) - 更新 main.go 使用 store.New() 替代 config.NewDatabase() - 更新 api/server.go 使用 *store.Store 替代 *config.Database - 更新 manager/trader_manager.go: - 新增 LoadTradersFromStore, LoadUserTradersFromStore 方法 - 删除旧版 LoadUserTraders, LoadTraderByID, loadSingleTrader 等方法 - 移除 nofx/config 依赖 - 删除 config/database.go 和 config/database_test.go - 更新 api/server_test.go 使用 store.Trader 类型 - 清理 logger/ 包中未使用的 telegram 相关代码 * refactor: unify encryption key management via .env - Remove redundant EncryptionManager and SecureStorage - Simplify CryptoService to load keys from environment variables only - RSA_PRIVATE_KEY: RSA private key for client-server encryption - DATA_ENCRYPTION_KEY: AES-256 key for database encryption - JWT_SECRET: JWT signing key for authentication - Update start.sh to auto-generate missing keys on first run - Remove secrets/ directory and file-based key storage - Delete obsolete encryption setup scripts - Update .env.example with all required keys * refactor: unify logger usage across mcp package - Add MCPLogger adapter in logger package to implement mcp.Logger interface - Update mcp/config.go to use global logger by default - Remove redundant defaultLogger from mcp/logger.go - Keep noopLogger for testing purposes * chore: remove leftover test RSA key file * chore: remove unused bootstrap package * refactor: unify logging to use logger package instead of fmt/log - Replace all fmt.Print/log.Print calls with logger package - Add auto-initialization in logger package init() for test compatibility - Update main.go to initialize logger at startup - Migrate all packages: api, backtest, config, decision, manager, market, store, trader * refactor: rename database file from config.db to data.db - Update main.go, start.sh, docker-compose.yml - Update migration script and documentation - Update .gitignore and translations * fix: add RSA_PRIVATE_KEY to docker-compose environment * fix: add registration_enabled to /api/config response * fix: Fix navigation between login and register pages Use window.location.href instead of react-router's navigate() to fix the issue where URL changes but the page doesn't reload due to App.tsx using custom route state management. * fix: Switch SQLite from WAL to DELETE mode for Docker compatibility WAL mode causes data sync issues with Docker bind mounts on macOS due to incompatible file locking mechanisms between the container and host. DELETE mode (traditional journaling) ensures data is written directly to the main database file. * refactor: Remove default user from database initialization The default user was a legacy placeholder that is no longer needed now that proper user registration is in place. * feat: Add order tracking system with centralized status sync - Add trader_orders table for tracking all order lifecycle - Implement GetOrderStatus interface for all exchanges (Binance, Bybit, Hyperliquid, Aster, Lighter) - Create OrderSyncManager for centralized order status polling - Add trading statistics (Sharpe ratio, win rate, profit factor) to AI context - Include recent completed orders in AI decision input - Remove per-order goroutine polling in favor of global sync manager * feat: Add TradingView K-line chart to dashboard - Create TradingViewChart component with exchange/symbol selectors - Support Binance, Bybit, OKX, Coinbase, Kraken, KuCoin exchanges - Add popular symbols quick selection - Support multiple timeframes (1m to 1W) - Add fullscreen mode - Integrate with Dashboard page below equity chart - Add i18n translations for zh/en * refactor: Replace separate charts with tabbed ChartTabs component - Create ChartTabs component with tab switching between equity curve and K-line - Add embedded mode support for EquityChart and TradingViewChart - User can now switch between account equity and market chart in same area * fix: Use ChartTabs in App.tsx and fix embedded mode in EquityChart - Replace EquityChart with ChartTabs in App.tsx (the actual dashboard renderer) - Fix EquityChart embedded mode for error and empty data states - Rename interval state to timeInterval to avoid shadowing window.setInterval - Add debug logging to ChartTabs component * feat: Add position tracking system for accurate trade history - Add trader_positions table to track complete open/close trades - Add PositionSyncManager to detect manual closes via polling - Record position on open, update on close with PnL calculation - Use positions table for trading stats and recent trades (replacing orders table) - Fix TradingView chart symbol format (add .P suffix for futures) - Fix DecisionCard wait/hold action color (gray instead of red) - Auto-append USDT suffix for custom symbol input * update ---------
This commit is contained in:
@@ -3,7 +3,7 @@ package config
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"nofx/logger"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -15,16 +15,7 @@ type LeverageConfig struct {
|
||||
|
||||
// LogConfig 日志配置
|
||||
type LogConfig struct {
|
||||
Level string `json:"level"` // 日志级别: debug, info, warn, error (默认: info)
|
||||
Telegram *TelegramConfig `json:"telegram"` // Telegram推送配置(可选)
|
||||
}
|
||||
|
||||
// TelegramConfig Telegram推送配置(简化版,只保留必需字段)
|
||||
type TelegramConfig struct {
|
||||
Enabled bool `json:"enabled"` // 是否启用(默认: false)
|
||||
BotToken string `json:"bot_token"` // Bot Token
|
||||
ChatID int64 `json:"chat_id"` // Chat ID
|
||||
MinLevel string `json:"min_level"` // 最低日志级别,该级别及以上的日志会推送到Telegram(可选,默认: error)
|
||||
Level string `json:"level"` // 日志级别: debug, info, warn, error (默认: info)
|
||||
}
|
||||
|
||||
// Config 总配置
|
||||
@@ -41,14 +32,14 @@ type Config struct {
|
||||
Leverage LeverageConfig `json:"leverage"`
|
||||
JWTSecret string `json:"jwt_secret"`
|
||||
DataKLineTime string `json:"data_k_line_time"`
|
||||
Log *LogConfig `json:"log"` // 日志配置
|
||||
Log *LogConfig `json:"nofx/logger"` // 日志配置
|
||||
}
|
||||
|
||||
// LoadConfig 从文件加载配置
|
||||
func LoadConfig(filename string) (*Config, error) {
|
||||
// 检查filename是否存在
|
||||
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||
log.Printf("📄 %s不存在,使用默认配置", filename)
|
||||
logger.Infof("📄 %s不存在,使用默认配置", filename)
|
||||
return &Config{}, nil
|
||||
}
|
||||
|
||||
|
||||
1735
config/database.go
1735
config/database.go
File diff suppressed because it is too large
Load Diff
@@ -1,850 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"nofx/crypto"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestUpdateExchange_EmptyValuesShouldNotOverwrite 测试空值不应覆盖现有数据
|
||||
// 这是 Bug 的核心:当前实现会用空字符串覆盖现有的私钥
|
||||
func TestUpdateExchange_EmptyValuesShouldNotOverwrite(t *testing.T) {
|
||||
// 准备测试数据库
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
userID := "test-user-001"
|
||||
|
||||
// 步骤 1: 创建初始配置(包含私钥)
|
||||
initialAPIKey := "initial-api-key-12345"
|
||||
initialSecretKey := "initial-secret-key-67890"
|
||||
|
||||
err := db.UpdateExchange(
|
||||
userID,
|
||||
"hyperliquid",
|
||||
true, // enabled
|
||||
initialAPIKey,
|
||||
initialSecretKey,
|
||||
false, // testnet
|
||||
"0xWalletAddress",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"", // lighter_wallet_addr
|
||||
"", // lighter_private_key
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 步骤 2: 验证初始数据已保存
|
||||
exchanges, err := db.GetExchanges(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("获取配置失败: %v", err)
|
||||
}
|
||||
if len(exchanges) == 0 {
|
||||
t.Fatal("未找到配置")
|
||||
}
|
||||
|
||||
// 解密后应该能看到原始值
|
||||
if exchanges[0].APIKey != initialAPIKey {
|
||||
t.Errorf("初始 APIKey 不正确,期望 %s,实际 %s", initialAPIKey, exchanges[0].APIKey)
|
||||
}
|
||||
|
||||
// 步骤 3: 用空值更新(模拟前端发送空值的场景)
|
||||
// 🐛 Bug 重现:这应该 NOT 覆盖现有的私钥,但当前实现会覆盖
|
||||
err = db.UpdateExchange(
|
||||
userID,
|
||||
"hyperliquid",
|
||||
false, // 只改变 enabled 状态
|
||||
"", // 空 apiKey - 不应该覆盖
|
||||
"", // 空 secretKey - 不应该覆盖
|
||||
true, // 改变 testnet 状态
|
||||
"0xWalletAddress",
|
||||
"",
|
||||
"",
|
||||
"", // 空 aster_private_key - 不应该覆盖
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("更新失败: %v", err)
|
||||
}
|
||||
|
||||
// 步骤 4: 验证私钥没有被空值覆盖
|
||||
exchanges, err = db.GetExchanges(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("获取更新后配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 🎯 关键断言:私钥应该保持不变
|
||||
if exchanges[0].APIKey != initialAPIKey {
|
||||
t.Errorf("❌ Bug 确认:APIKey 被空值覆盖了!期望 %s,实际 %s", initialAPIKey, exchanges[0].APIKey)
|
||||
}
|
||||
if exchanges[0].SecretKey != initialSecretKey {
|
||||
t.Errorf("❌ Bug 确认:SecretKey 被空值覆盖了!期望 %s,实际 %s", initialSecretKey, exchanges[0].SecretKey)
|
||||
}
|
||||
|
||||
// 验证非敏感字段正常更新
|
||||
if exchanges[0].Enabled {
|
||||
t.Error("enabled 应该被更新为 false")
|
||||
}
|
||||
if !exchanges[0].Testnet {
|
||||
t.Error("testnet 应该被更新为 true")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateExchange_AsterEmptyValuesShouldNotOverwrite 测试 Aster 私钥不被空值覆盖
|
||||
func TestUpdateExchange_AsterEmptyValuesShouldNotOverwrite(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
userID := "test-user-002"
|
||||
|
||||
// 步骤 1: 创建 Aster 配置
|
||||
initialAsterKey := "aster-private-key-xyz123"
|
||||
|
||||
err := db.UpdateExchange(
|
||||
userID,
|
||||
"aster",
|
||||
true,
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
"0xAsterUser",
|
||||
"0xAsterSigner",
|
||||
initialAsterKey,
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化 Aster 失败: %v", err)
|
||||
}
|
||||
|
||||
// 步骤 2: 用空值更新
|
||||
err = db.UpdateExchange(
|
||||
userID,
|
||||
"aster",
|
||||
false, // 只改 enabled
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
"",
|
||||
"0xAsterUser",
|
||||
"0xAsterSigner",
|
||||
"", // 空 aster_private_key
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("更新失败: %v", err)
|
||||
}
|
||||
|
||||
// 步骤 3: 验证 aster_private_key 没有被覆盖
|
||||
exchanges, err := db.GetExchanges(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("获取配置失败: %v", err)
|
||||
}
|
||||
|
||||
if exchanges[0].AsterPrivateKey != initialAsterKey {
|
||||
t.Errorf("❌ Bug 确认:AsterPrivateKey 被空值覆盖了!期望 %s,实际 %s", initialAsterKey, exchanges[0].AsterPrivateKey)
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateExchange_NonEmptyValuesShouldUpdate 测试非空值应该正常更新
|
||||
func TestUpdateExchange_NonEmptyValuesShouldUpdate(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
userID := "test-user-003"
|
||||
|
||||
// 步骤 1: 创建初始配置
|
||||
err := db.UpdateExchange(
|
||||
userID,
|
||||
"hyperliquid",
|
||||
true,
|
||||
"old-api-key",
|
||||
"old-secret-key",
|
||||
false,
|
||||
"0xOldWallet",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 步骤 2: 用非空值更新
|
||||
newAPIKey := "new-api-key-456"
|
||||
newSecretKey := "new-secret-key-789"
|
||||
|
||||
err = db.UpdateExchange(
|
||||
userID,
|
||||
"hyperliquid",
|
||||
true,
|
||||
newAPIKey,
|
||||
newSecretKey,
|
||||
false,
|
||||
"0xNewWallet",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("更新失败: %v", err)
|
||||
}
|
||||
|
||||
// 步骤 3: 验证新值已更新
|
||||
exchanges, err := db.GetExchanges(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("获取配置失败: %v", err)
|
||||
}
|
||||
|
||||
if exchanges[0].APIKey != newAPIKey {
|
||||
t.Errorf("APIKey 未更新,期望 %s,实际 %s", newAPIKey, exchanges[0].APIKey)
|
||||
}
|
||||
if exchanges[0].SecretKey != newSecretKey {
|
||||
t.Errorf("SecretKey 未更新,期望 %s,实际 %s", newSecretKey, exchanges[0].SecretKey)
|
||||
}
|
||||
if exchanges[0].HyperliquidWalletAddr != "0xNewWallet" {
|
||||
t.Errorf("WalletAddr 未更新")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateExchange_PartialUpdateShouldWork 测试部分字段更新
|
||||
func TestUpdateExchange_PartialUpdateShouldWork(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
userID := "test-user-005"
|
||||
|
||||
// 创建初始配置
|
||||
err := db.UpdateExchange(
|
||||
userID,
|
||||
"hyperliquid",
|
||||
true,
|
||||
"api-key-123",
|
||||
"secret-key-456",
|
||||
false,
|
||||
"0xWallet1",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 只更新 enabled 和 testnet,私钥留空
|
||||
err = db.UpdateExchange(
|
||||
userID,
|
||||
"hyperliquid",
|
||||
false,
|
||||
"", // 留空
|
||||
"", // 留空
|
||||
true,
|
||||
"0xWallet2",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("部分更新失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证
|
||||
exchanges, err := db.GetExchanges(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("获取配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 私钥应该保持不变
|
||||
if exchanges[0].APIKey != "api-key-123" {
|
||||
t.Errorf("APIKey 不应改变,期望 api-key-123,实际 %s", exchanges[0].APIKey)
|
||||
}
|
||||
if exchanges[0].SecretKey != "secret-key-456" {
|
||||
t.Errorf("SecretKey 不应改变,期望 secret-key-456,实际 %s", exchanges[0].SecretKey)
|
||||
}
|
||||
|
||||
// 其他字段应该更新
|
||||
if exchanges[0].Enabled {
|
||||
t.Error("enabled 应该更新为 false")
|
||||
}
|
||||
if !exchanges[0].Testnet {
|
||||
t.Error("testnet 应该更新为 true")
|
||||
}
|
||||
if exchanges[0].HyperliquidWalletAddr != "0xWallet2" {
|
||||
t.Error("wallet 地址应该更新")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateExchange_MultipleExchangeTypes 测试不同交易所类型
|
||||
func TestUpdateExchange_MultipleExchangeTypes(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
userID := "test-user-006"
|
||||
|
||||
testCases := []struct {
|
||||
exchangeID string
|
||||
name string
|
||||
typ string
|
||||
}{
|
||||
{"binance", "Binance Futures", "cex"},
|
||||
{"hyperliquid", "Hyperliquid", "dex"},
|
||||
{"aster", "Aster DEX", "dex"},
|
||||
{"unknown-exchange", "unknown-exchange Exchange", "cex"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.exchangeID, func(t *testing.T) {
|
||||
err := db.UpdateExchange(
|
||||
userID,
|
||||
tc.exchangeID,
|
||||
true,
|
||||
"api-key-"+tc.exchangeID,
|
||||
"secret-key-"+tc.exchangeID,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("创建 %s 失败: %v", tc.exchangeID, err)
|
||||
}
|
||||
|
||||
// 验证创建成功
|
||||
exchanges, err := db.GetExchanges(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("获取配置失败: %v", err)
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, ex := range exchanges {
|
||||
if ex.ID == tc.exchangeID {
|
||||
found = true
|
||||
if ex.Name != tc.name {
|
||||
t.Errorf("交易所名称不正确,期望 %s,实际 %s", tc.name, ex.Name)
|
||||
}
|
||||
if ex.Type != tc.typ {
|
||||
t.Errorf("交易所类型不正确,期望 %s,实际 %s", tc.typ, ex.Type)
|
||||
}
|
||||
if ex.APIKey != "api-key-"+tc.exchangeID {
|
||||
t.Errorf("APIKey 不正确")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("未找到交易所 %s", tc.exchangeID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateExchange_MixedSensitiveFields 测试混合更新敏感和非敏感字段
|
||||
func TestUpdateExchange_MixedSensitiveFields(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
userID := "test-user-007"
|
||||
|
||||
// 创建初始配置
|
||||
err := db.UpdateExchange(
|
||||
userID,
|
||||
"hyperliquid",
|
||||
true,
|
||||
"old-api-key",
|
||||
"old-secret-key",
|
||||
false,
|
||||
"0xOldWallet",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 场景1: 只更新 apiKey,secretKey 留空
|
||||
err = db.UpdateExchange(
|
||||
userID,
|
||||
"hyperliquid",
|
||||
false,
|
||||
"new-api-key",
|
||||
"", // 留空
|
||||
true,
|
||||
"0xNewWallet",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("更新1失败: %v", err)
|
||||
}
|
||||
|
||||
exchanges, _ := db.GetExchanges(userID)
|
||||
if exchanges[0].APIKey != "new-api-key" {
|
||||
t.Error("APIKey 应该更新")
|
||||
}
|
||||
if exchanges[0].SecretKey != "old-secret-key" {
|
||||
t.Error("SecretKey 应该保持不变")
|
||||
}
|
||||
|
||||
// 场景2: 只更新 secretKey,apiKey 留空
|
||||
err = db.UpdateExchange(
|
||||
userID,
|
||||
"hyperliquid",
|
||||
true,
|
||||
"", // 留空
|
||||
"new-secret-key",
|
||||
false,
|
||||
"0xFinalWallet",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("更新2失败: %v", err)
|
||||
}
|
||||
|
||||
exchanges, _ = db.GetExchanges(userID)
|
||||
if exchanges[0].APIKey != "new-api-key" {
|
||||
t.Error("APIKey 应该保持不变")
|
||||
}
|
||||
if exchanges[0].SecretKey != "new-secret-key" {
|
||||
t.Error("SecretKey 应该更新")
|
||||
}
|
||||
if exchanges[0].Enabled != true {
|
||||
t.Error("Enabled 应该更新为 true")
|
||||
}
|
||||
if exchanges[0].HyperliquidWalletAddr != "0xFinalWallet" {
|
||||
t.Error("WalletAddr 应该更新")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateExchange_OnlyNonSensitiveFields 测试只更新非敏感字段
|
||||
func TestUpdateExchange_OnlyNonSensitiveFields(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
userID := "test-user-008"
|
||||
|
||||
// 创建初始配置(包含所有私钥)
|
||||
err := db.UpdateExchange(
|
||||
userID,
|
||||
"aster",
|
||||
true,
|
||||
"binance-api",
|
||||
"binance-secret",
|
||||
false,
|
||||
"",
|
||||
"0xUser1",
|
||||
"0xSigner1",
|
||||
"aster-private-key-1",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 只更新非敏感字段(所有私钥字段留空)
|
||||
err = db.UpdateExchange(
|
||||
userID,
|
||||
"aster",
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
true,
|
||||
"",
|
||||
"0xUser2",
|
||||
"0xSigner2",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("更新失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证所有私钥保持不变
|
||||
exchanges, _ := db.GetExchanges(userID)
|
||||
if exchanges[0].APIKey != "binance-api" {
|
||||
t.Errorf("APIKey 应该保持不变,实际 %s", exchanges[0].APIKey)
|
||||
}
|
||||
if exchanges[0].SecretKey != "binance-secret" {
|
||||
t.Errorf("SecretKey 应该保持不变,实际 %s", exchanges[0].SecretKey)
|
||||
}
|
||||
if exchanges[0].AsterPrivateKey != "aster-private-key-1" {
|
||||
t.Errorf("AsterPrivateKey 应该保持不变,实际 %s", exchanges[0].AsterPrivateKey)
|
||||
}
|
||||
|
||||
// 验证非敏感字段已更新
|
||||
if exchanges[0].Enabled != false {
|
||||
t.Error("Enabled 应该更新为 false")
|
||||
}
|
||||
if exchanges[0].Testnet != true {
|
||||
t.Error("Testnet 应该更新为 true")
|
||||
}
|
||||
if exchanges[0].AsterUser != "0xUser2" {
|
||||
t.Error("AsterUser 应该更新")
|
||||
}
|
||||
if exchanges[0].AsterSigner != "0xSigner2" {
|
||||
t.Error("AsterSigner 应该更新")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateExchange_AllSensitiveFieldsUpdate 测试同时更新所有敏感字段
|
||||
func TestUpdateExchange_AllSensitiveFieldsUpdate(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
userID := "test-user-009"
|
||||
|
||||
// 创建初始配置
|
||||
err := db.UpdateExchange(
|
||||
userID,
|
||||
"binance",
|
||||
true,
|
||||
"old-api",
|
||||
"old-secret",
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"old-aster-key",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化失败: %v", err)
|
||||
}
|
||||
|
||||
// 同时更新所有敏感字段
|
||||
err = db.UpdateExchange(
|
||||
userID,
|
||||
"binance",
|
||||
false,
|
||||
"new-api",
|
||||
"new-secret",
|
||||
true,
|
||||
"0xWallet",
|
||||
"0xUser",
|
||||
"0xSigner",
|
||||
"new-aster-key",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("更新失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证所有字段都更新了
|
||||
exchanges, _ := db.GetExchanges(userID)
|
||||
if exchanges[0].APIKey != "new-api" {
|
||||
t.Error("APIKey 应该更新")
|
||||
}
|
||||
if exchanges[0].SecretKey != "new-secret" {
|
||||
t.Error("SecretKey 应该更新")
|
||||
}
|
||||
if exchanges[0].AsterPrivateKey != "new-aster-key" {
|
||||
t.Error("AsterPrivateKey 应该更新")
|
||||
}
|
||||
if !exchanges[0].Testnet {
|
||||
t.Error("Testnet 应该更新为 true")
|
||||
}
|
||||
}
|
||||
|
||||
// setupTestDB 创建测试数据库
|
||||
func setupTestDB(t *testing.T) (*Database, func()) {
|
||||
// 创建临时数据库文件
|
||||
tmpFile := t.TempDir() + "/test.db"
|
||||
|
||||
db, err := NewDatabase(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("创建测试数据库失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建测试用户
|
||||
testUsers := []string{
|
||||
"test-user-001", "test-user-002", "test-user-003", "test-user-004", "test-user-005",
|
||||
"test-user-006", "test-user-007", "test-user-008", "test-user-009",
|
||||
"test-user-persistence", "user1", "user2",
|
||||
}
|
||||
for _, userID := range testUsers {
|
||||
user := &User{
|
||||
ID: userID,
|
||||
Email: userID + "@test.com",
|
||||
PasswordHash: "hash",
|
||||
OTPSecret: "",
|
||||
OTPVerified: false,
|
||||
}
|
||||
_ = db.CreateUser(user)
|
||||
}
|
||||
|
||||
// 设置加密服务(用于测试加密功能)
|
||||
// 创建临时 RSA 密钥
|
||||
rsaKeyPath := t.TempDir() + "/test_rsa_key"
|
||||
cryptoService, err := crypto.NewCryptoService(rsaKeyPath)
|
||||
if err != nil {
|
||||
// 如果创建失败,继续测试但不使用加密
|
||||
t.Logf("警告:无法创建加密服务,将在无加密模式下测试: %v", err)
|
||||
} else {
|
||||
db.SetCryptoService(cryptoService)
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
db.Close()
|
||||
os.RemoveAll(tmpFile)
|
||||
os.RemoveAll(rsaKeyPath)
|
||||
}
|
||||
|
||||
return db, cleanup
|
||||
}
|
||||
|
||||
// TestWALModeEnabled 测试 WAL 模式是否启用
|
||||
// TDD: 这个测试应该失败,因为当前代码没有启用 WAL 模式
|
||||
func TestWALModeEnabled(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// 查询当前的 journal_mode
|
||||
var journalMode string
|
||||
err := db.db.QueryRow("PRAGMA journal_mode").Scan(&journalMode)
|
||||
if err != nil {
|
||||
t.Fatalf("查询 journal_mode 失败: %v", err)
|
||||
}
|
||||
|
||||
// 期望是 WAL 模式
|
||||
if journalMode != "wal" {
|
||||
t.Errorf("期望 journal_mode=wal,实际是 %s", journalMode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSynchronousMode 测试 synchronous 模式设置
|
||||
// TDD: 验证数据持久性设置
|
||||
func TestSynchronousMode(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// 查询 synchronous 设置
|
||||
var synchronous int
|
||||
err := db.db.QueryRow("PRAGMA synchronous").Scan(&synchronous)
|
||||
if err != nil {
|
||||
t.Fatalf("查询 synchronous 失败: %v", err)
|
||||
}
|
||||
|
||||
// 期望是 FULL (2) 以确保数据持久性
|
||||
if synchronous != 2 {
|
||||
t.Errorf("期望 synchronous=2 (FULL),实际是 %d", synchronous)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDataPersistenceAcrossReopen 测试数据在数据库关闭并重新打开后是否持久化
|
||||
// TDD: 模拟 Docker restart 场景
|
||||
func TestDataPersistenceAcrossReopen(t *testing.T) {
|
||||
// 创建临时数据库文件
|
||||
tmpFile, err := os.CreateTemp("", "test_persistence_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("创建临时文件失败: %v", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
dbPath := tmpFile.Name()
|
||||
defer os.Remove(dbPath)
|
||||
|
||||
// 设置加密服务
|
||||
rsaKeyPath := "test_rsa_key.pem"
|
||||
cryptoService, err := crypto.NewCryptoService(rsaKeyPath)
|
||||
if err != nil {
|
||||
t.Fatalf("初始化加密服务失败: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(rsaKeyPath)
|
||||
|
||||
userID := "test-user-persistence"
|
||||
testAPIKey := "test-api-key-should-persist"
|
||||
testSecretKey := "test-secret-key-should-persist"
|
||||
|
||||
// 第一次打开数据库并写入数据
|
||||
{
|
||||
db, err := NewDatabase(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("第一次创建数据库失败: %v", err)
|
||||
}
|
||||
db.SetCryptoService(cryptoService)
|
||||
|
||||
// 创建持久化测试用户,避免外键约束失败
|
||||
_ = db.CreateUser(&User{
|
||||
ID: userID,
|
||||
Email: userID + "@test.com",
|
||||
PasswordHash: "hash",
|
||||
OTPSecret: "",
|
||||
OTPVerified: true,
|
||||
})
|
||||
|
||||
// 写入交易所配置
|
||||
err = db.UpdateExchange(
|
||||
userID,
|
||||
"binance",
|
||||
true,
|
||||
testAPIKey,
|
||||
testSecretKey,
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("写入数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 模拟正常关闭
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatalf("关闭数据库失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 第二次打开数据库并验证数据是否还在
|
||||
{
|
||||
db, err := NewDatabase(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("第二次打开数据库失败: %v", err)
|
||||
}
|
||||
db.SetCryptoService(cryptoService)
|
||||
defer db.Close()
|
||||
|
||||
// 读取数据
|
||||
exchanges, err := db.GetExchanges(userID)
|
||||
if err != nil {
|
||||
t.Fatalf("读取数据失败: %v", err)
|
||||
}
|
||||
|
||||
if len(exchanges) == 0 {
|
||||
t.Fatal("数据丢失:没有找到任何交易所配置")
|
||||
}
|
||||
|
||||
// 验证数据完整性
|
||||
found := false
|
||||
for _, ex := range exchanges {
|
||||
if ex.ID == "binance" {
|
||||
found = true
|
||||
if ex.APIKey != testAPIKey {
|
||||
t.Errorf("API Key 丢失或损坏,期望 %s,实际 %s", testAPIKey, ex.APIKey)
|
||||
}
|
||||
if ex.SecretKey != testSecretKey {
|
||||
t.Errorf("Secret Key 丢失或损坏,期望 %s,实际 %s", testSecretKey, ex.SecretKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("数据丢失:找不到 binance 配置")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConcurrentWritesWithWAL 测试 WAL 模式下的并发写入
|
||||
// TDD: WAL 模式应该支持更好的并发性能
|
||||
func TestConcurrentWritesWithWAL(t *testing.T) {
|
||||
db, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// 这个测试验证多个并发写入可以成功
|
||||
// WAL 模式下并发性能更好,但 SQLite 仍然可能出现短暂的锁
|
||||
done := make(chan bool, 2)
|
||||
errors := make(chan error, 10)
|
||||
|
||||
// 并发写入1
|
||||
go func() {
|
||||
for i := 0; i < 3; i++ {
|
||||
err := db.UpdateExchange(
|
||||
"user1",
|
||||
"binance",
|
||||
true,
|
||||
"key1",
|
||||
"secret1",
|
||||
false,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
}
|
||||
// 小延迟减少锁冲突
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// 并发写入2
|
||||
go func() {
|
||||
for i := 0; i < 3; i++ {
|
||||
err := db.UpdateExchange(
|
||||
"user2",
|
||||
"hyperliquid",
|
||||
true,
|
||||
"key2",
|
||||
"secret2",
|
||||
false,
|
||||
"0xWallet",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
}
|
||||
// 小延迟减少锁冲突
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
// 等待两个 goroutine 完成
|
||||
<-done
|
||||
<-done
|
||||
close(errors)
|
||||
|
||||
// 检查是否有错误
|
||||
errorCount := 0
|
||||
for err := range errors {
|
||||
t.Logf("并发写入错误: %v", err)
|
||||
errorCount++
|
||||
}
|
||||
|
||||
// WAL 模式下应该能处理并发,但可能有少量锁错误
|
||||
// 我们允许最多 2 个错误
|
||||
if errorCount > 2 {
|
||||
t.Errorf("并发写入失败次数过多: %d", errorCount)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4Y666RzY5LLi6PiYL+vC
|
||||
7+fcr122Fd8BC7IdqUSYKQ33Nsi9J7J5fDgcMf7ZAnIBpxMV7+e1KEoiwtGmxwHj
|
||||
mYo0ZV0E6JXdiK26S052+Shquri0IXkwGFraDuNKqmGrj6vZuXtq2L2gdSyZCxrI
|
||||
veN9g6LxBvLBP1Rx7UEmZeyokRYvChcxAQXuS/0br44BOHGtwAElk6AGLISz55AG
|
||||
oM40b3ktiza+8THKMz3GiylQQYpBltbM3yAXPlnXJ2MtUZiaHNhEQI4++PMvEErN
|
||||
Izm8cIgcvUAXJ5vBfa4kD0kSgBJFuEQ2im3qcWTuEPRKztEeJDY7XAVHc1Xy6d4N
|
||||
vQIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
Reference in New Issue
Block a user