package roomadmin import ( "context" "fmt" "strconv" "strings" "hyapp-admin-server/internal/integration/robotclient" "hyapp-admin-server/internal/integration/roomclient" "hyapp-admin-server/internal/modules/shared" ) const availableRoomRobotPageSize = int32(200) type RobotRoom struct { AppCode string `json:"appCode"` RoomID string `json:"roomId"` RoomShortID string `json:"roomShortId"` Title string `json:"title"` CoverURL string `json:"coverUrl"` VisibleRegionID int64 `json:"visibleRegionId"` RegionName string `json:"regionName"` Status string `json:"status"` OwnerRobotUserID string `json:"ownerRobotUserId"` Owner RoomOwner `json:"owner"` RobotUserIDs []string `json:"robotUserIds"` Robots []RoomOwner `json:"robots"` ActiveRobotCount int32 `json:"activeRobotCount"` SeatCount int32 `json:"seatCount"` GiftRule RobotGiftRule `json:"giftRule"` CreatedByAdminID uint64 `json:"createdByAdminId"` CreatedAtMS int64 `json:"createdAtMs"` UpdatedAtMS int64 `json:"updatedAtMs"` } type RobotGiftRule struct { GiftIDs []string `json:"giftIds"` LuckyGiftIDs []string `json:"luckyGiftIds"` NormalGiftIntervalMS int64 `json:"normalGiftIntervalMs"` LuckyComboMin int64 `json:"luckyComboMin"` LuckyComboMax int64 `json:"luckyComboMax"` LuckyPauseMinMS int64 `json:"luckyPauseMinMs"` LuckyPauseMaxMS int64 `json:"luckyPauseMaxMs"` RobotStayMinMS int64 `json:"robotStayMinMs"` RobotStayMaxMS int64 `json:"robotStayMaxMs"` RobotReplaceMinMS int64 `json:"robotReplaceMinMs"` RobotReplaceMaxMS int64 `json:"robotReplaceMaxMs"` MaxGiftSenders int64 `json:"maxGiftSenders"` } type AvailableRoomRobot struct { UserID string `json:"userId"` Status string `json:"status"` RoomScene string `json:"roomScene"` LastUsedAtMS int64 `json:"lastUsedAtMs"` UsedCount int64 `json:"usedCount"` User RoomOwner `json:"user"` } type HumanRoomRobotConfig struct { AppCode string `json:"appCode"` Enabled bool `json:"enabled"` CandidateRoomMaxOnline int32 `json:"candidateRoomMaxOnline"` RoomFullStopOnline int32 `json:"roomFullStopOnline"` RoomTargetMinOnline int32 `json:"roomTargetMinOnline"` RoomTargetMaxOnline int32 `json:"roomTargetMaxOnline"` RobotStayMinMS int64 `json:"robotStayMinMs"` RobotStayMaxMS int64 `json:"robotStayMaxMs"` RobotReplaceMinMS int64 `json:"robotReplaceMinMs"` RobotReplaceMaxMS int64 `json:"robotReplaceMaxMs"` NormalGiftIntervalMS int64 `json:"normalGiftIntervalMs"` NormalGiftIntervalMinMS int64 `json:"normalGiftIntervalMinMs"` NormalGiftIntervalMaxMS int64 `json:"normalGiftIntervalMaxMs"` GiftIDs []string `json:"giftIds"` LuckyGiftIDs []string `json:"luckyGiftIds"` SuperLuckyGiftIDs []string `json:"superLuckyGiftIds"` LuckyComboMin int64 `json:"luckyComboMin"` LuckyComboMax int64 `json:"luckyComboMax"` LuckyPauseMinMS int64 `json:"luckyPauseMinMs"` LuckyPauseMaxMS int64 `json:"luckyPauseMaxMs"` MaxGiftSenders int64 `json:"maxGiftSenders"` UpdatedByAdminID uint64 `json:"updatedByAdminId"` CreatedAtMS int64 `json:"createdAtMs"` UpdatedAtMS int64 `json:"updatedAtMs"` } func (s *Service) ListRobotRooms(ctx context.Context, query robotRoomListQuery) ([]RobotRoom, int64, error) { if s.roomClient == nil { return nil, 0, fmt.Errorf("room service client is not configured") } if query.Page <= 0 { query.Page = 1 } if query.PageSize <= 0 || query.PageSize > 100 { query.PageSize = 50 } result, err := s.roomClient.ListRobotRooms(ctx, roomclient.ListRobotRoomsRequest{ Page: query.Page, PageSize: query.PageSize, Status: normalizeRobotRoomListStatus(query.Status), }) if err != nil { return nil, 0, err } items := robotRoomsFromClient(result.Rooms) if err := s.fillRobotRoomDetails(ctx, items); err != nil { return nil, 0, err } return items, result.Total, nil } func normalizeRobotRoomListStatus(status string) string { switch strings.TrimSpace(status) { case "active", "stopped": return strings.TrimSpace(status) default: return "" } } func (s *Service) ListAvailableRoomRobots(ctx context.Context) ([]AvailableRoomRobot, error) { if s.robotClient == nil || s.roomClient == nil { return nil, fmt.Errorf("robot or room service client is not configured") } robots, err := listAllActiveGameRobots(ctx, s.robotClient) if err != nil { return nil, err } userIDs := make([]int64, 0, len(robots)) for _, item := range robots { if item.UserID > 0 { userIDs = append(userIDs, item.UserID) } } // 全站机器人是后台统一维护的机器人账号池;房间机器人下拉只把这批账号交给 room-service 做占用过滤, // 避免新增了全站机器人却因为旧的 room-scene 专用池为空而无法选择房主机器人。 available, err := s.roomClient.FilterAvailableRoomRobots(ctx, userIDs) if err != nil { return nil, err } owners, err := s.queryRoomOwners(ctx, available.AvailableUserIDs) if err != nil { return nil, err } return availableRoomRobotsFromGamePool(robots, available.AvailableUserIDs, owners), nil } func listAllActiveGameRobots(ctx context.Context, client robotclient.Client) ([]robotclient.Robot, error) { if client == nil { return nil, fmt.Errorf("robot service client is not configured") } cursor := "" seenCursors := make(map[string]bool) robots := make([]robotclient.Robot, 0, availableRoomRobotPageSize) for { // 机器人房间下拉要先拿到完整全站 active 机器人池,再统一交给 room-service 做占用过滤; // 只取第一页会在测试服机器人数量超过 200 或第一页大多已占用时,把后续地区的新机器人挡在下拉外。 result, err := client.ListGameRobots(ctx, robotclient.ListGameRobotsRequest{ Status: "active", PageSize: availableRoomRobotPageSize, Cursor: cursor, }) if err != nil { return nil, err } robots = append(robots, result.Robots...) next := strings.TrimSpace(result.NextCursor) if next == "" { return robots, nil } if seenCursors[next] { return nil, fmt.Errorf("robot service returned repeated cursor %q", next) } seenCursors[next] = true cursor = next } } func (s *Service) GetHumanRoomRobotConfig(ctx context.Context) (HumanRoomRobotConfig, error) { if s.roomClient == nil { return HumanRoomRobotConfig{}, fmt.Errorf("room service client is not configured") } config, err := s.roomClient.GetHumanRoomRobotConfig(ctx) if err != nil { return HumanRoomRobotConfig{}, err } return s.humanRoomRobotConfigFromClient(ctx, config) } func (s *Service) UpdateHumanRoomRobotConfig(ctx context.Context, req updateHumanRoomRobotConfigRequest, actor shared.Actor) (HumanRoomRobotConfig, error) { if s.roomClient == nil { return HumanRoomRobotConfig{}, fmt.Errorf("room service client is not configured") } config := roomclient.HumanRoomRobotConfig{ Enabled: req.Enabled, CandidateRoomMaxOnline: req.CandidateRoomMaxOnline, RoomFullStopOnline: req.RoomFullStopOnline, RoomTargetMinOnline: req.RoomTargetMinOnline, RoomTargetMaxOnline: req.RoomTargetMaxOnline, RobotStayMinMS: req.RobotStayMinMS, RobotStayMaxMS: req.RobotStayMaxMS, RobotReplaceMinMS: req.RobotReplaceMinMS, RobotReplaceMaxMS: req.RobotReplaceMaxMS, NormalGiftIntervalMS: req.NormalGiftIntervalMS, NormalGiftIntervalMinMS: req.NormalGiftIntervalMinMS, NormalGiftIntervalMaxMS: req.NormalGiftIntervalMaxMS, GiftIDs: normalizeStringList(req.GiftIDs), LuckyGiftIDs: normalizeStringList(req.LuckyGiftIDs), SuperLuckyGiftIDs: normalizeStringList(req.SuperLuckyGiftIDs), LuckyComboMin: req.LuckyComboMin, LuckyComboMax: req.LuckyComboMax, LuckyPauseMinMS: req.LuckyPauseMinMS, LuckyPauseMaxMS: req.LuckyPauseMaxMS, MaxGiftSenders: req.MaxGiftSenders, } saved, err := s.roomClient.UpdateHumanRoomRobotConfig(ctx, roomclient.UpdateHumanRoomRobotConfigRequest{ Config: config, AdminID: uint64(actor.UserID), }) if err != nil { return HumanRoomRobotConfig{}, mapRoomClientError(err) } return s.humanRoomRobotConfigFromClient(ctx, saved) } func availableRoomRobotsFromGamePool(robots []robotclient.Robot, availableUserIDs []int64, owners map[int64]RoomOwner) []AvailableRoomRobot { allowed := make(map[int64]bool, len(availableUserIDs)) for _, userID := range availableUserIDs { allowed[userID] = true } items := make([]AvailableRoomRobot, 0, len(availableUserIDs)) for _, robot := range robots { if !allowed[robot.UserID] { continue } items = append(items, AvailableRoomRobot{ UserID: strconv.FormatInt(robot.UserID, 10), Status: robot.Status, RoomScene: robot.RoomScene, LastUsedAtMS: robot.LastUsedAtMS, UsedCount: robot.UsedCount, User: owners[robot.UserID], }) } return items } func normalizeStringList(values []string) []string { seen := make(map[string]bool, len(values)) out := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" || seen[value] { continue } seen[value] = true out = append(out, value) } return out } func (s *Service) humanRoomRobotConfigFromClient(_ context.Context, config roomclient.HumanRoomRobotConfig) (HumanRoomRobotConfig, error) { out := HumanRoomRobotConfig{ AppCode: config.AppCode, Enabled: config.Enabled, CandidateRoomMaxOnline: config.CandidateRoomMaxOnline, RoomFullStopOnline: config.RoomFullStopOnline, RoomTargetMinOnline: config.RoomTargetMinOnline, RoomTargetMaxOnline: config.RoomTargetMaxOnline, RobotStayMinMS: config.RobotStayMinMS, RobotStayMaxMS: config.RobotStayMaxMS, RobotReplaceMinMS: config.RobotReplaceMinMS, RobotReplaceMaxMS: config.RobotReplaceMaxMS, NormalGiftIntervalMS: config.NormalGiftIntervalMS, NormalGiftIntervalMinMS: config.NormalGiftIntervalMinMS, NormalGiftIntervalMaxMS: config.NormalGiftIntervalMaxMS, GiftIDs: append([]string(nil), config.GiftIDs...), LuckyGiftIDs: append([]string(nil), config.LuckyGiftIDs...), SuperLuckyGiftIDs: append([]string(nil), config.SuperLuckyGiftIDs...), LuckyComboMin: config.LuckyComboMin, LuckyComboMax: config.LuckyComboMax, LuckyPauseMinMS: config.LuckyPauseMinMS, LuckyPauseMaxMS: config.LuckyPauseMaxMS, MaxGiftSenders: config.MaxGiftSenders, UpdatedByAdminID: config.UpdatedByAdminID, CreatedAtMS: config.CreatedAtMS, UpdatedAtMS: config.UpdatedAtMS, } return out, nil } func (s *Service) CreateRobotRoom(ctx context.Context, req createRobotRoomRequest, actor shared.Actor) (RobotRoom, error) { if s.roomClient == nil { return RobotRoom{}, fmt.Errorf("room service client is not configured") } normalized, err := normalizeCreateRobotRoomRequest(req) if err != nil { return RobotRoom{}, err } req = normalized if err := s.applyOwnerProfileToRobotRoomRequest(ctx, &req); err != nil { return RobotRoom{}, err } created, err := s.roomClient.CreateRobotRoom(ctx, roomclient.CreateRobotRoomRequest{ OwnerRobotUserID: int64(req.OwnerRobotUserID), CandidateRobotUserIDs: []int64(req.CandidateRobotUserIDs), MinRobotCount: req.MinRobotCount, MaxRobotCount: req.MaxRobotCount, RoomName: req.RoomName, RoomAvatar: req.RoomAvatar, VisibleRegionID: req.VisibleRegionID, OwnerCountryCode: req.OwnerCountryCode, SeatCount: req.SeatCount, GiftRule: roomclient.RobotRoomGiftRule{ GiftIDs: req.GiftIDs, LuckyGiftIDs: req.LuckyGiftIDs, NormalGiftIntervalMS: req.NormalGiftIntervalMS, LuckyComboMin: req.LuckyComboMin, LuckyComboMax: req.LuckyComboMax, LuckyPauseMinMS: req.LuckyPauseMinMS, LuckyPauseMaxMS: req.LuckyPauseMaxMS, RobotStayMinMS: req.RobotStayMinMS, RobotStayMaxMS: req.RobotStayMaxMS, RobotReplaceMinMS: req.RobotReplaceMinMS, RobotReplaceMaxMS: req.RobotReplaceMaxMS, MaxGiftSenders: req.MaxGiftSenders, }, AdminID: uint64(actor.UserID), }) if err != nil { return RobotRoom{}, mapRoomClientError(err) } items := []RobotRoom{robotRoomFromClient(created)} if err := s.fillRobotRoomDetails(ctx, items); err != nil { return RobotRoom{}, err } return items[0], nil } func (s *Service) applyOwnerProfileToRobotRoomRequest(ctx context.Context, req *createRobotRoomRequest) error { if s.userDB == nil { return fmt.Errorf("user database is not configured") } ownerID := int64(req.OwnerRobotUserID) owners, err := s.queryRoomOwners(ctx, []int64{ownerID}) if err != nil { return err } owner, ok := owners[ownerID] if !ok { return fmt.Errorf("%w: 房主机器人资料不存在", ErrInvalidArgument) } return applyOwnerProfileToRobotRoomRequest(req, owner) } func applyOwnerProfileToRobotRoomRequest(req *createRobotRoomRequest, owner RoomOwner) error { roomName, roomAvatar, err := robotRoomProfileFromOwner(owner) if err != nil { return err } // 机器人房间对外展示和区域归属必须跟随房主机器人资料,避免后台表单或脚本传入的自定义值让房间卡片、区域桶和国家筛选不一致。 req.RoomName = roomName req.RoomAvatar = roomAvatar req.VisibleRegionID = owner.VisibleRegionID req.OwnerCountryCode = strings.ToUpper(strings.TrimSpace(owner.CountryCode)) return nil } func robotRoomProfileFromOwner(owner RoomOwner) (string, string, error) { name := strings.TrimSpace(owner.Username) avatar := strings.TrimSpace(owner.Avatar) if name == "" { return "", "", fmt.Errorf("%w: 房主机器人名称不能为空", ErrInvalidArgument) } if avatar == "" { return "", "", fmt.Errorf("%w: 房主机器人头像不能为空", ErrInvalidArgument) } return name, avatar, nil } func (s *Service) SetRobotRoomStatus(ctx context.Context, roomID string, status string, actor shared.Actor) (RobotRoom, error) { if s.roomClient == nil { return RobotRoom{}, fmt.Errorf("room service client is not configured") } roomID = strings.TrimSpace(roomID) status = strings.TrimSpace(status) if roomID == "" || (status != "active" && status != "stopped") { return RobotRoom{}, fmt.Errorf("%w: 机器人房间状态参数不正确", ErrInvalidArgument) } item, err := s.roomClient.SetRobotRoomStatus(ctx, roomclient.SetRobotRoomStatusRequest{ RoomID: roomID, Status: status, AdminID: uint64(actor.UserID), }) if err != nil { return RobotRoom{}, mapRoomClientError(err) } items := []RobotRoom{robotRoomFromClient(item)} if err := s.fillRobotRoomDetails(ctx, items); err != nil { return RobotRoom{}, err } return items[0], nil } func normalizeCreateRobotRoomRequest(req createRobotRoomRequest) (createRobotRoomRequest, error) { ownerID := int64(req.OwnerRobotUserID) if ownerID <= 0 { return req, fmt.Errorf("%w: 请选择房主机器人", ErrInvalidArgument) } if req.MinRobotCount <= 0 || req.MaxRobotCount < req.MinRobotCount { return req, fmt.Errorf("%w: 机器人数量范围不正确", ErrInvalidArgument) } if req.SeatCount <= 0 { req.SeatCount = 10 } if !validRobotRoomSeatCount(req.SeatCount) { return req, fmt.Errorf("%w: 麦位数不正确", ErrInvalidArgument) } candidateIDs := normalizeRobotRoomCandidateIDs([]int64(req.CandidateRobotUserIDs), ownerID) if len(candidateIDs) == 0 { return req, fmt.Errorf("%w: 请选择候选机器人", ErrInvalidArgument) } if len(req.GiftIDs) == 0 { return req, fmt.Errorf("%w: 请选择普通礼物合集", ErrInvalidArgument) } if len(req.LuckyGiftIDs) == 0 { return req, fmt.Errorf("%w: 请选择幸运礼物合集", ErrInvalidArgument) } if req.NormalGiftIntervalMS <= 0 { return req, fmt.Errorf("%w: 普通礼物频次不正确", ErrInvalidArgument) } if req.LuckyComboMin <= 0 || req.LuckyComboMax < req.LuckyComboMin { return req, fmt.Errorf("%w: 幸运礼物连击范围不正确", ErrInvalidArgument) } if req.LuckyPauseMinMS <= 0 || req.LuckyPauseMaxMS < req.LuckyPauseMinMS { return req, fmt.Errorf("%w: 幸运礼物间隔范围不正确", ErrInvalidArgument) } if req.RobotStayMinMS < 0 || req.RobotStayMaxMS < req.RobotStayMinMS { return req, fmt.Errorf("%w: 机器人停留时间范围不正确", ErrInvalidArgument) } if req.RobotReplaceMinMS < 0 || req.RobotReplaceMaxMS < req.RobotReplaceMinMS { return req, fmt.Errorf("%w: 机器人补位时间范围不正确", ErrInvalidArgument) } if req.MaxGiftSenders < 0 { return req, fmt.Errorf("%w: 同时送礼机器人数量不正确", ErrInvalidArgument) } req.CandidateRobotUserIDs = flexibleJSONInt64Slice(candidateIDs) return req, nil } func validRobotRoomSeatCount(seatCount int32) bool { switch seatCount { case 10, 15, 20: return true default: return false } } func normalizeRobotRoomCandidateIDs(values []int64, ownerID int64) []int64 { seen := make(map[int64]bool, len(values)) out := make([]int64, 0, len(values)) for _, value := range values { // 房主由 room-service 单独并入机器人集合;候选列表只保留其他机器人,避免前端重复选择或旧脚本传入重复 ID。 if value <= 0 || value == ownerID || seen[value] { continue } seen[value] = true out = append(out, value) } return out } func robotRoomsFromClient(items []roomclient.RobotRoom) []RobotRoom { out := make([]RobotRoom, 0, len(items)) for _, item := range items { out = append(out, robotRoomFromClient(item)) } return out } func robotRoomFromClient(item roomclient.RobotRoom) RobotRoom { robotUserIDs := make([]string, 0, len(item.RobotUserIDs)) for _, userID := range item.RobotUserIDs { robotUserIDs = append(robotUserIDs, strconv.FormatInt(userID, 10)) } return RobotRoom{ AppCode: item.AppCode, RoomID: item.RoomID, RoomShortID: item.RoomShortID, Title: item.Title, CoverURL: item.CoverURL, VisibleRegionID: item.VisibleRegionID, Status: item.Status, OwnerRobotUserID: strconv.FormatInt(item.OwnerRobotUserID, 10), RobotUserIDs: robotUserIDs, ActiveRobotCount: item.ActiveRobotCount, SeatCount: item.SeatCount, GiftRule: RobotGiftRule{ GiftIDs: item.GiftRule.GiftIDs, LuckyGiftIDs: item.GiftRule.LuckyGiftIDs, NormalGiftIntervalMS: item.GiftRule.NormalGiftIntervalMS, LuckyComboMin: item.GiftRule.LuckyComboMin, LuckyComboMax: item.GiftRule.LuckyComboMax, LuckyPauseMinMS: item.GiftRule.LuckyPauseMinMS, LuckyPauseMaxMS: item.GiftRule.LuckyPauseMaxMS, RobotStayMinMS: item.GiftRule.RobotStayMinMS, RobotStayMaxMS: item.GiftRule.RobotStayMaxMS, RobotReplaceMinMS: item.GiftRule.RobotReplaceMinMS, RobotReplaceMaxMS: item.GiftRule.RobotReplaceMaxMS, MaxGiftSenders: item.GiftRule.MaxGiftSenders, }, CreatedByAdminID: item.CreatedByAdminID, CreatedAtMS: item.CreatedAtMS, UpdatedAtMS: item.UpdatedAtMS, } } func (s *Service) fillRobotRoomDetails(ctx context.Context, items []RobotRoom) error { if s.userDB == nil || len(items) == 0 { return nil } userIDs := make([]int64, 0) regionIDs := make([]int64, 0) for _, item := range items { if ownerID, err := strconv.ParseInt(item.OwnerRobotUserID, 10, 64); err == nil && ownerID > 0 { userIDs = append(userIDs, ownerID) } for _, rawID := range item.RobotUserIDs { if userID, err := strconv.ParseInt(rawID, 10, 64); err == nil && userID > 0 { userIDs = append(userIDs, userID) } } if item.VisibleRegionID > 0 { regionIDs = append(regionIDs, item.VisibleRegionID) } } owners, err := s.queryRoomOwners(ctx, uniqueInt64s(userIDs)) if err != nil { return err } regions, err := s.queryRegionNames(ctx, uniqueInt64s(regionIDs)) if err != nil { return err } for index := range items { if ownerID, err := strconv.ParseInt(items[index].OwnerRobotUserID, 10, 64); err == nil { items[index].Owner = owners[ownerID] } for _, rawID := range items[index].RobotUserIDs { if userID, err := strconv.ParseInt(rawID, 10, 64); err == nil { items[index].Robots = append(items[index].Robots, owners[userID]) } } items[index].RegionName = regions[items[index].VisibleRegionID] } return nil }