From bb79845b6bb295860d6a9a7329ad3880b33a619e Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 16 Jun 2026 20:47:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=B8=E6=88=8F=E6=9C=BA=E5=99=A8=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gamemanagement/robot_profile_source.go | 49 ++++++++++++++++++- .../robot_profile_source_test.go | 44 +++++++++++++++++ .../gamemanagement/self_game_handler.go | 23 +++++---- 3 files changed, 105 insertions(+), 11 deletions(-) diff --git a/server/admin/internal/modules/gamemanagement/robot_profile_source.go b/server/admin/internal/modules/gamemanagement/robot_profile_source.go index 099deb20..b58d7edc 100644 --- a/server/admin/internal/modules/gamemanagement/robot_profile_source.go +++ b/server/admin/internal/modules/gamemanagement/robot_profile_source.go @@ -2,8 +2,10 @@ package gamemanagement import ( "context" + "crypto/rand" "database/sql" "fmt" + "math/big" "net/url" "strings" "unicode/utf8" @@ -12,6 +14,7 @@ import ( type RobotProfile struct { Nickname string Avatar string + Gender string } type RobotProfileSource interface { @@ -29,7 +32,7 @@ func NewLikeiRobotProfileSource(db *sql.DB) RobotProfileSource { return &likeiRobotProfileSource{db: db} } -// WithRobotProfileSource 注入机器人资料池;创建机器人时只消费这里返回的线上 likei 用户昵称和头像。 +// WithRobotProfileSource 注入机器人资料池;创建机器人优先消费线上 likei 用户昵称和头像,未配置时再走本地随机资料兜底。 func WithRobotProfileSource(source RobotProfileSource) Option { return func(h *Handler) { h.robotProfiles = source @@ -81,6 +84,50 @@ func (s *likeiRobotProfileSource) RandomRobotProfiles(ctx context.Context, count return profiles, nil } +func robotProfilesForGenerate(ctx context.Context, source RobotProfileSource, count int) ([]RobotProfile, error) { + if source != nil { + // 已配置 likei 时继续使用真实资料池;查询失败不能静默降级,否则线上数据源故障会被批量造号掩盖。 + return source.RandomRobotProfiles(ctx, count) + } + profiles := make([]RobotProfile, 0, count) + for index := 0; index < count; index++ { + profiles = append(profiles, randomRobotProfile()) + } + return profiles, nil +} + +func randomRobotProfile() RobotProfile { + seed := randomRobotHex(8) + nameSeed := randomRobotHex(3) + if seed == "fallback" { + seed = fmt.Sprintf("fallback-%d", randomRobotInt(1000000)) + } + // 本地随机头像只保存绝对 URL,不在 admin-server 里下载或转存图片;user-service 仍负责资料字段校验。 + return RobotProfile{ + Nickname: fmt.Sprintf("Robot %s", strings.ToUpper(nameSeed)), + Avatar: "https://api.dicebear.com/9.x/adventurer/png?seed=hyapp-robot-" + url.QueryEscape(seed), + Gender: randomRobotGender(), + } +} + +func randomRobotGender() string { + if randomRobotInt(2) == 0 { + return "male" + } + return "female" +} + +func randomRobotInt(max int64) int64 { + if max <= 0 { + return 0 + } + value, err := rand.Int(rand.Reader, big.NewInt(max)) + if err != nil { + return 0 + } + return value.Int64() +} + func normalizeRobotProfile(profile RobotProfile) (RobotProfile, bool) { profile.Nickname = strings.TrimSpace(profile.Nickname) profile.Avatar = strings.TrimSpace(profile.Avatar) diff --git a/server/admin/internal/modules/gamemanagement/robot_profile_source_test.go b/server/admin/internal/modules/gamemanagement/robot_profile_source_test.go index baf22a82..2a7589b5 100644 --- a/server/admin/internal/modules/gamemanagement/robot_profile_source_test.go +++ b/server/admin/internal/modules/gamemanagement/robot_profile_source_test.go @@ -5,6 +5,7 @@ import ( "context" "net/http" "net/http/httptest" + "strings" "testing" "hyapp-admin-server/internal/integration/gameclient" @@ -104,3 +105,46 @@ func TestGenerateDiceRobotsUsesLikeiProfiles(t *testing.T) { t.Fatalf("registered user ids = %#v", games.registered) } } + +func TestGenerateDiceRobotsFallsBackToRandomProfilesWhenLikeiMissing(t *testing.T) { + gin.SetMode(gin.TestMode) + users := &fakeRobotUserClient{} + games := &fakeRobotGameClient{} + handler := New(games, users, nil) + + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Set(middleware.ContextRequestID, "req-robot-random") + c.Set(middleware.ContextUserID, uint(7)) + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/games/self/dice/robots:generate", bytes.NewBufferString(`{ + "count": 3, + "country": "TH" + }`)) + c.Request.Header.Set("Content-Type", "application/json") + + handler.GenerateDiceRobots(c) + + if recorder.Code != http.StatusOK { + t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String()) + } + if len(users.created) != 3 { + t.Fatalf("created users = %d, want 3", len(users.created)) + } + for index, created := range users.created { + if !strings.HasPrefix(created.Username, "Robot ") { + t.Fatalf("created[%d] username should be generated, got %#v", index, created) + } + if !strings.HasPrefix(created.Avatar, "https://api.dicebear.com/9.x/adventurer/png?seed=hyapp-robot-") { + t.Fatalf("created[%d] avatar should be generated, got %#v", index, created) + } + if created.Gender != "male" && created.Gender != "female" { + t.Fatalf("created[%d] gender should be random male/female, got %#v", index, created) + } + if created.Country != "TH" || created.Source != "game_robot" { + t.Fatalf("created[%d] metadata = %#v", index, created) + } + } + if len(games.registered) != 3 || games.registered[0] != 9001 || games.registered[2] != 9003 { + t.Fatalf("registered user ids = %#v", games.registered) + } +} diff --git a/server/admin/internal/modules/gamemanagement/self_game_handler.go b/server/admin/internal/modules/gamemanagement/self_game_handler.go index 77184a62..c85adfec 100644 --- a/server/admin/internal/modules/gamemanagement/self_game_handler.go +++ b/server/admin/internal/modules/gamemanagement/self_game_handler.go @@ -220,19 +220,14 @@ func (h *Handler) GenerateDiceRobots(c *gin.Context) { if country == "" { country = "SA" } - if h.robotProfiles == nil { - response.ServerError(c, "likei 机器人资料源未配置") - return - } - // 新机器人资料只从 likei 线上用户库随机抽取;后台不再使用本地昵称池、随机头像站或 COS 转存, - // 这样每次批量创建都面对真实用户资料池,避免少量固定预设反复出现在匹配列表里。 - profiles, err := h.robotProfiles.RandomRobotProfiles(c.Request.Context(), int(req.Count)) + // likei 是优先资料源;测试服或本地未配置时用随机资料兜底,避免运维为了造机器人账号被外部历史库阻塞。 + profiles, err := robotProfilesForGenerate(c.Request.Context(), h.robotProfiles, int(req.Count)) if err != nil { - response.BadRequest(c, "获取 likei 机器人资料失败: "+err.Error()) + response.BadRequest(c, "获取机器人资料失败: "+err.Error()) return } if len(profiles) < int(req.Count) { - response.BadRequest(c, "likei 机器人资料数量不足") + response.BadRequest(c, "机器人资料数量不足") return } userIDs := make([]int64, 0, req.Count) @@ -240,13 +235,21 @@ func (h *Handler) GenerateDiceRobots(c *gin.Context) { accountLanguage := robotAccountLanguage(req.NicknameLanguage) for index := int32(0); index < req.Count; index++ { profile := profiles[index] + gender := strings.TrimSpace(req.Gender) + if gender == "" { + gender = strings.TrimSpace(profile.Gender) + } + if gender == "" { + // likei 当前只提供昵称和头像;没有显式性别时保持旧的均匀分布,随机兜底资料会在 profile 中给出真实随机值。 + gender = robotGender(int(index), "") + } created, err := h.user.QuickCreateAccount(c.Request.Context(), userclient.QuickCreateAccountRequest{ RequestID: requestID, Caller: "admin-server", Password: randomRobotPassword(), Username: profile.Nickname, Avatar: profile.Avatar, - Gender: robotGender(int(index), req.Gender), + Gender: gender, Country: country, DeviceID: "game-robot-" + randomRobotHex(8), Source: "game_robot",