2026-06-18 17:13:56 +08:00

560 lines
20 KiB
Go

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
}