From 8a1e931857560de907ac69f2ee0d128242d039ea Mon Sep 17 00:00:00 2001 From: ZhouYongyou <128128010+zhouyongyou@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:54:36 +0800 Subject: [PATCH] fix: prevent initial_balance modification and fix equity history calculation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 核心問題修復 1. **API 層保護** (api/server.go - handleUpdateTrader) - 檢測並阻止任何修改 initial_balance 的嘗試 - 強制使用原始 initial_balance 值 - 記錄詳細的警告日誌和嘗試修改的細節 - 返回友善的錯誤訊息給用戶 2. **資料庫層保護** (config/database.go - UpdateTrader) - 從 UPDATE SQL 語句中移除 initial_balance 欄位 - 雙重防護:即使 API 層被繞過,DB 也不會更新 3. **修復盈虧計算錯誤** (api/server.go - handleEquityHistory) - ✅ 修復:從資料庫讀取 initial_balance 作為唯一真實來源 - ❌ 移除:錯誤的後備邏輯(使用 records[0].TotalBalance) - ✅ 重新計算:基於正確的 initial_balance 重算所有盈虧百分比 ## 影響範圍 - 用戶無法再通過 UpdateTrader API 修改 initial_balance - 解決「初始餘額異常變動」的根本原因 - 確保盈虧計算始終基於正確的基準值 ## 技術細節 - 浮點數比較容差:0.01 USDT (避免浮點數精度問題) - 錯誤碼:INITIAL_BALANCE_IMMUTABLE - 日誌格式:包含 user_id, trader_id, 原值, 請求值, 差異 Related-Issue: 用戶報告「初始餘額變少」問題 --- api/server.go | 65 +++++++++++++++++++++++++++++++--------------- config/database.go | 4 +-- 2 files changed, 46 insertions(+), 23 deletions(-) diff --git a/api/server.go b/api/server.go index a41d6374..5394629e 100644 --- a/api/server.go +++ b/api/server.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "log" + "math" "net/http" "nofx/auth" "nofx/config" @@ -527,6 +528,40 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { systemPromptTemplate = existingTrader.SystemPromptTemplate // 保持原值 } + // 🔒 保護 initial_balance 不被修改 + initialBalance := existingTrader.InitialBalance + if req.InitialBalance > 0 { + // 檢查是否嘗試修改初始餘額(允許 0.01 USDT 的誤差) + diff := math.Abs(req.InitialBalance - existingTrader.InitialBalance) + if diff > 0.01 { + // 記錄警告日誌 + log.Printf("⚠️ BLOCKED: User %s attempted to modify initial_balance | Trader=%s | Original=%.2f | Requested=%.2f | Diff=%.2f", + userID, traderID, existingTrader.InitialBalance, req.InitialBalance, diff) + + c.JSON(http.StatusBadRequest, gin.H{ + "error": "不允許修改初始餘額", + "code": "INITIAL_BALANCE_IMMUTABLE", + "details": gin.H{ + "current_value": existingTrader.InitialBalance, + "requested_value": req.InitialBalance, + "difference": diff, + "trader_id": traderID, + "trader_name": existingTrader.Name, + "created_at": existingTrader.CreatedAt, + }, + "message": fmt.Sprintf( + "初始餘額是固定的基準值,創建後不可修改。\n\n"+ + "當前初始餘額: %.2f USDT\n"+ + "嘗試修改為: %.2f USDT\n\n"+ + "如確實需要修改,請聯繫管理員。", + existingTrader.InitialBalance, + req.InitialBalance, + ), + }) + return + } + } + // 更新交易员配置 trader := &config.TraderRecord{ ID: traderID, @@ -534,7 +569,7 @@ func (s *Server) handleUpdateTrader(c *gin.Context) { Name: req.Name, AIModelID: req.AIModelID, ExchangeID: req.ExchangeID, - InitialBalance: req.InitialBalance, + InitialBalance: initialBalance, BTCETHLeverage: btcEthLeverage, AltcoinLeverage: altcoinLeverage, TradingSymbols: req.TradingSymbols, @@ -1195,6 +1230,7 @@ func (s *Server) handleCompetition(c *gin.Context) { // handleEquityHistory 收益率历史数据 func (s *Server) handleEquityHistory(c *gin.Context) { + userID := c.GetString("user_id") _, traderID, err := s.getTraderFromQuery(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) @@ -1229,19 +1265,13 @@ func (s *Server) handleEquityHistory(c *gin.Context) { CycleNumber int `json:"cycle_number"` } - // 从AutoTrader获取初始余额(用于计算盈亏百分比) - initialBalance := 0.0 - if status := trader.GetStatus(); status != nil { - if ib, ok := status["initial_balance"].(float64); ok && ib > 0 { - initialBalance = ib - } + traderRecord, _, _, err := s.database.GetTraderConfig(userID, traderID) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "无法获取交易员配置"}) + return } - // 如果无法从status获取,且有历史记录,则从第一条记录获取 - if initialBalance == 0 && len(records) > 0 { - // 第一条记录的equity作为初始余额 - initialBalance = records[0].AccountState.TotalBalance - } + initialBalance := traderRecord.InitialBalance // 如果还是无法获取,返回错误 if initialBalance == 0 { @@ -1253,16 +1283,9 @@ func (s *Server) handleEquityHistory(c *gin.Context) { var history []EquityPoint for _, record := range records { - // TotalBalance字段实际存储的是TotalEquity totalEquity := record.AccountState.TotalBalance - // TotalUnrealizedProfit字段实际存储的是TotalPnL(相对初始余额) - totalPnL := record.AccountState.TotalUnrealizedProfit - - // 计算盈亏百分比 - totalPnLPct := 0.0 - if initialBalance > 0 { - totalPnLPct = (totalPnL / initialBalance) * 100 - } + totalPnL := totalEquity - initialBalance + totalPnLPct := (totalPnL / initialBalance) * 100 history = append(history, EquityPoint{ Timestamp: record.Timestamp.Format("2006-01-02 15:04:05"), diff --git a/config/database.go b/config/database.go index 2afc0528..b2500972 100644 --- a/config/database.go +++ b/config/database.go @@ -845,12 +845,12 @@ func (d *Database) UpdateTraderStatus(userID, id string, isRunning bool) error { func (d *Database) UpdateTrader(trader *TraderRecord) error { _, err := d.db.Exec(` UPDATE traders SET - name = ?, ai_model_id = ?, exchange_id = ?, initial_balance = ?, + 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.InitialBalance, + `, 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)