游戏机器人

This commit is contained in:
zhx 2026-06-16 20:47:26 +08:00
parent 83b82b8e30
commit bb79845b6b
3 changed files with 105 additions and 11 deletions

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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",