From c1cf44b98fd8d2861ea1b7a65cd0f43c1587a529 Mon Sep 17 00:00:00 2001 From: 0xYYBB | ZYY | Bobo <128128010+the-dev-z@users.noreply.github.com> Date: Fri, 14 Nov 2025 23:33:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20use=20UUID=20to=20ensure=20traderID?= =?UTF-8?q?=20uniqueness=20(#893)=20(#1008)=20##=20Problem=20When=20multip?= =?UTF-8?q?le=20users=20create=20traders=20with=20the=20same=20exchange=20?= =?UTF-8?q?+=20AI=20model=20combination=20within=20the=20same=20second,=20?= =?UTF-8?q?they=20generate=20identical=20traderIDs,=20causing=20data=20con?= =?UTF-8?q?flicts.=20Old=20code=20(Line=20496):=20```go=20traderID=20:=3D?= =?UTF-8?q?=20fmt.Sprintf("%s=5F%s=5F%d",=20req.ExchangeID,=20req.AIModelI?= =?UTF-8?q?D,=20time.Now().Unix())=20```=20##=20Solution=20Use=20UUID=20to?= =?UTF-8?q?=20guarantee=20100%=20uniqueness=20while=20preserving=20prefix?= =?UTF-8?q?=20for=20debugging:=20```go=20traderID=20:=3D=20fmt.Sprintf("%s?= =?UTF-8?q?=5F%s=5F%s",=20req.ExchangeID,=20req.AIModelID,=20uuid.New().St?= =?UTF-8?q?ring())=20```=20Example=20output:=20`binance=5Fgpt-4=5Fa1b2c3d4?= =?UTF-8?q?-e5f6-7890-abcd-ef1234567890`=20##=20Changes=20-=20`api/server.?= =?UTF-8?q?go:495-497`:=20Use=20UUID=20for=20traderID=20generation=20-=20`?= =?UTF-8?q?api/traderid=5Ftest.go`:=20New=20test=20file=20with=203=20compr?= =?UTF-8?q?ehensive=20tests=20##=20Tests=20=E2=9C=85=20All=20tests=20passe?= =?UTF-8?q?d=20(0.189s)=20=E2=9C=85=20TestTraderIDUniqueness=20-=20100=20u?= =?UTF-8?q?nique=20IDs=20generated=20=E2=9C=85=20TestTraderIDFormat=20-=20?= =?UTF-8?q?3=20exchange/model=20combinations=20validated=20=E2=9C=85=20Tes?= =?UTF-8?q?tTraderIDNoCollision=20-=201000=20iterations=20without=20collis?= =?UTF-8?q?ion=20Fixes=20#893=20Co-authored-by:=20the-dev-z=20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/server.go | 5 +- api/traderid_test.go | 123 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 api/traderid_test.go diff --git a/api/server.go b/api/server.go index 3aadcc24..daf3665e 100644 --- a/api/server.go +++ b/api/server.go @@ -492,8 +492,9 @@ func (s *Server) handleCreateTrader(c *gin.Context) { } } - // 生成交易员ID - traderID := fmt.Sprintf("%s_%s_%d", req.ExchangeID, req.AIModelID, time.Now().Unix()) + // 生成交易员ID (使用 UUID 确保唯一性,解决 Issue #893) + // 保留前缀以便调试和日志追踪 + traderID := fmt.Sprintf("%s_%s_%s", req.ExchangeID, req.AIModelID, uuid.New().String()) // 设置默认值 isCrossMargin := true // 默认为全仓模式 diff --git a/api/traderid_test.go b/api/traderid_test.go new file mode 100644 index 00000000..52b581c8 --- /dev/null +++ b/api/traderid_test.go @@ -0,0 +1,123 @@ +package api + +import ( + "fmt" + "strings" + "testing" + + "github.com/google/uuid" +) + +// TestTraderIDUniqueness 测试 traderID 的唯一性(修复 Issue #893) +// 验证即使在相同的 exchange 和 AI model 下,也能生成唯一的 traderID +func TestTraderIDUniqueness(t *testing.T) { + exchangeID := "binance" + aiModelID := "gpt-4" + + // 模拟同时创建 100 个 trader(相同参数) + traderIDs := make(map[string]bool) + const numTraders = 100 + + for i := 0; i < numTraders; i++ { + // 模拟 api/server.go:497 的 traderID 生成逻辑 + traderID := generateTraderID(exchangeID, aiModelID) + + // ✅ 检查是否重复 + if traderIDs[traderID] { + t.Errorf("Duplicate traderID detected: %s", traderID) + } + traderIDs[traderID] = true + + // ✅ 验证格式:应该是 "exchange_model_uuid" + if !isValidTraderIDFormat(traderID, exchangeID, aiModelID) { + t.Errorf("Invalid traderID format: %s", traderID) + } + } + + // ✅ 验证生成了预期数量的唯一 ID + if len(traderIDs) != numTraders { + t.Errorf("Expected %d unique traderIDs, got %d", numTraders, len(traderIDs)) + } +} + +// generateTraderID 辅助函数,模拟 api/server.go 中的 traderID 生成逻辑 +func generateTraderID(exchangeID, aiModelID string) string { + return fmt.Sprintf("%s_%s_%s", exchangeID, aiModelID, uuid.New().String()) +} + +// isValidTraderIDFormat 验证 traderID 格式是否符合预期 +func isValidTraderIDFormat(traderID, expectedExchange, expectedModel string) bool { + // 格式:exchange_model_uuid + // 例如:binance_gpt-4_a1b2c3d4-e5f6-7890-abcd-ef1234567890 + parts := strings.Split(traderID, "_") + if len(parts) < 3 { + return false + } + + // 验证前缀 + if parts[0] != expectedExchange { + return false + } + + // AI model 可能包含连字符(如 gpt-4),所以需要重组 + // 最后一部分应该是 UUID + uuidPart := parts[len(parts)-1] + + // 验证 UUID 格式(36 个字符,包含 4 个连字符) + _, err := uuid.Parse(uuidPart) + return err == nil +} + +// TestTraderIDFormat 测试 traderID 格式的正确性 +func TestTraderIDFormat(t *testing.T) { + tests := []struct { + name string + exchangeID string + aiModelID string + }{ + {"Binance + GPT-4", "binance", "gpt-4"}, + {"Hyperliquid + Claude", "hyperliquid", "claude-3"}, + {"OKX + Qwen", "okx", "qwen-2.5"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + traderID := generateTraderID(tt.exchangeID, tt.aiModelID) + + // ✅ 验证包含正确的前缀 + if !strings.HasPrefix(traderID, tt.exchangeID+"_"+tt.aiModelID+"_") { + t.Errorf("traderID does not have correct prefix. Got: %s", traderID) + } + + // ✅ 验证格式有效 + if !isValidTraderIDFormat(traderID, tt.exchangeID, tt.aiModelID) { + t.Errorf("Invalid traderID format: %s", traderID) + } + + // ✅ 验证长度合理(至少应该有 exchange + model + "_" + UUID(36) 的长度) + minLength := len(tt.exchangeID) + len(tt.aiModelID) + 2 + 36 // 2个下划线 + 36字符UUID + if len(traderID) < minLength { + t.Errorf("traderID too short: expected at least %d chars, got %d", minLength, len(traderID)) + } + }) + } +} + +// TestTraderIDNoCollision 测试在高并发场景下不会产生碰撞 +func TestTraderIDNoCollision(t *testing.T) { + const iterations = 1000 + uniqueIDs := make(map[string]bool, iterations) + + // 模拟高并发场景 + for i := 0; i < iterations; i++ { + id := generateTraderID("binance", "gpt-4") + if uniqueIDs[id] { + t.Fatalf("Collision detected after %d iterations: %s", i+1, id) + } + uniqueIDs[id] = true + } + + if len(uniqueIDs) != iterations { + t.Errorf("Expected %d unique IDs, got %d", iterations, len(uniqueIDs)) + } +}