package gamemanagement import ( "context" "errors" "strconv" "strings" "hyapp-admin-server/internal/integration/userclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/response" gamev1 "hyapp.local/api/proto/game/v1" "github.com/gin-gonic/gin" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) const gameWhitelistInternalUserIDMinDigits = 15 var errGameWhitelistUserNotFound = errors.New("game whitelist user not found") type gameWhitelistEnabledRequest struct { Enabled bool `json:"enabled"` } type gameWhitelistUserRequest struct { // 保留 userId JSON 字段兼容已发布管理端;值允许内部 user_id、默认短号或当前有效靓号。 UserID string `json:"userId"` } // gameWhitelistUserResolver 把白名单解析限制在 user-service owner API,避免 admin-server 直查用户库复制身份口径。 type gameWhitelistUserResolver interface { GetUser(context.Context, userclient.GetUserRequest) (*userclient.User, error) ResolveDisplayUserID(context.Context, userclient.ResolveDisplayUserIDRequest) (*userclient.UserIdentity, error) } type gameWhitelistUserDTO struct { UserID string `json:"userId"` DisplayUserID string `json:"displayUserId"` Username string `json:"username"` Avatar string `json:"avatar"` Status string `json:"status"` CreatedByAdminID int64 `json:"createdByAdminId"` CreatedAtMS int64 `json:"createdAtMs"` } // SetGameWhitelistEnabled 只切换访问策略,不隐式增删成员;空白名单开启后应明确表现为无人可见。 func (h *Handler) SetGameWhitelistEnabled(c *gin.Context) { gameID := strings.TrimSpace(c.Param("game_id")) var req gameWhitelistEnabledRequest if gameID == "" || c.ShouldBindJSON(&req) != nil { response.BadRequest(c, "游戏白名单参数不正确") return } resp, err := h.game.SetGameWhitelistEnabled(c.Request.Context(), &gamev1.SetGameWhitelistEnabledRequest{ Meta: requestMeta(c), GameId: gameID, Enabled: req.Enabled, }) if err != nil { response.BadRequest(c, err.Error()) return } h.auditLog(c, "set-game-whitelist-enabled", "game_catalog", gameID, strconv.FormatBool(req.Enabled)) response.OK(c, catalogFromProto(resp.GetGame())) } // ListGameWhitelistUsers 从 game-service 读取成员事实,再批量补 user-service 展示资料,避免复制用户主数据到游戏库。 func (h *Handler) ListGameWhitelistUsers(c *gin.Context) { gameID := strings.TrimSpace(c.Param("game_id")) if gameID == "" { response.BadRequest(c, "游戏 ID 参数不正确") return } resp, err := h.game.ListGameWhitelistUsers(c.Request.Context(), &gamev1.ListGameWhitelistUsersRequest{ Meta: requestMeta(c), GameId: gameID, PageSize: int32(parsePositiveInt(c.Query("pageSize"), 200)), }) if err != nil { response.ServerError(c, "获取游戏白名单失败") return } userIDs := make([]int64, 0, len(resp.GetUsers())) for _, item := range resp.GetUsers() { userIDs = append(userIDs, item.GetUserId()) } profiles := map[int64]*userclient.User{} if len(userIDs) > 0 { profiles, err = h.user.BatchGetUsers(c.Request.Context(), userclient.BatchGetUsersRequest{ RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", UserIDs: userIDs, }) if err != nil { response.ServerError(c, "获取白名单用户资料失败") return } } items := make([]gameWhitelistUserDTO, 0, len(resp.GetUsers())) for _, item := range resp.GetUsers() { items = append(items, gameWhitelistUserFromProto(item, profiles[item.GetUserId()])) } response.OK(c, gin.H{"items": items, "serverTimeMs": resp.GetServerTimeMs()}) } // AddGameWhitelistUser 先把内部 ID、默认短号或当前有效靓号统一解析为 owner user_id,再写游戏访问成员表。 func (h *Handler) AddGameWhitelistUser(c *gin.Context) { gameID := strings.TrimSpace(c.Param("game_id")) var req gameWhitelistUserRequest if gameID == "" || c.ShouldBindJSON(&req) != nil { response.BadRequest(c, "游戏白名单用户参数不正确") return } keyword := strings.TrimSpace(req.UserID) if keyword == "" { response.BadRequest(c, "请输入用户 ID、短号或靓号") return } profile, targetKind, err := resolveGameWhitelistUser(c.Request.Context(), h.user, middleware.CurrentRequestID(c), keyword) if errors.Is(err, errGameWhitelistUserNotFound) { response.BadRequest(c, "未找到匹配的用户") return } if err != nil || profile == nil { response.ServerError(c, "匹配用户失败") return } resp, err := h.game.AddGameWhitelistUser(c.Request.Context(), &gamev1.AddGameWhitelistUserRequest{ Meta: requestMeta(c), GameId: gameID, UserId: profile.UserID, }) if err != nil { response.BadRequest(c, err.Error()) return } // 审计只记录最终内部 user_id 和解析类型,避免后续短号/靓号变更让历史记录失去指向。 h.auditLog(c, "add-game-whitelist-user", "game_user_whitelist", gameID+":"+strconv.FormatInt(profile.UserID, 10), "added target_kind="+targetKind) response.OK(c, gameWhitelistUserFromProto(resp.GetUser(), profile)) } // resolveGameWhitelistUser 延续后台现有身份优先级:短数字和字母靓号优先按展示号解析,雪花长整型优先按内部 ID 读取。 func resolveGameWhitelistUser(ctx context.Context, resolver gameWhitelistUserResolver, requestID string, keyword string) (*userclient.User, string, error) { keyword = strings.TrimSpace(keyword) if resolver == nil || keyword == "" { return nil, "", errGameWhitelistUserNotFound } numericID, numericErr := strconv.ParseInt(keyword, 10, 64) numericOK := numericErr == nil && numericID > 0 userIDChecked := false // 生产内部 user_id 是雪花长整型;先查长数字可保持已发布内部 ID 输入语义,同时仍允许长数字靓号在未命中后回退解析。 if numericOK && len(strings.TrimLeft(keyword, "0")) >= gameWhitelistInternalUserIDMinDigits { userIDChecked = true if profile, found, err := getGameWhitelistUser(ctx, resolver, requestID, numericID); err != nil { return nil, "", err } else if found { return profile, "user_id", nil } } identity, err := resolver.ResolveDisplayUserID(ctx, userclient.ResolveDisplayUserIDRequest{ RequestID: requestID, Caller: "admin-server", DisplayUserID: keyword, }) if err == nil && identity != nil && identity.UserID > 0 { profile, found, getErr := getGameWhitelistUser(ctx, resolver, requestID, identity.UserID) if getErr != nil { return nil, "", getErr } if found { return profile, "display_user_id", nil } } else if err != nil && status.Code(err) != codes.NotFound && status.Code(err) != codes.InvalidArgument { // user-service 不可用等系统错误不能伪装成用户不存在,否则运营重试时无法判断真实故障。 return nil, "", err } if numericOK && !userIDChecked { if profile, found, err := getGameWhitelistUser(ctx, resolver, requestID, numericID); err != nil { return nil, "", err } else if found { return profile, "user_id", nil } } return nil, "", errGameWhitelistUserNotFound } func getGameWhitelistUser(ctx context.Context, resolver gameWhitelistUserResolver, requestID string, userID int64) (*userclient.User, bool, error) { profile, err := resolver.GetUser(ctx, userclient.GetUserRequest{RequestID: requestID, Caller: "admin-server", UserID: userID}) if status.Code(err) == codes.NotFound || profile == nil && err == nil { return nil, false, nil } if err != nil { return nil, false, err } return profile, true, nil } func (h *Handler) DeleteGameWhitelistUser(c *gin.Context) { gameID := strings.TrimSpace(c.Param("game_id")) userID, err := strconv.ParseInt(strings.TrimSpace(c.Param("user_id")), 10, 64) if gameID == "" || err != nil || userID <= 0 { response.BadRequest(c, "游戏白名单用户参数不正确") return } if _, err := h.game.DeleteGameWhitelistUser(c.Request.Context(), &gamev1.DeleteGameWhitelistUserRequest{ Meta: requestMeta(c), GameId: gameID, UserId: userID, }); err != nil { response.BadRequest(c, err.Error()) return } h.auditLog(c, "delete-game-whitelist-user", "game_user_whitelist", gameID+":"+strconv.FormatInt(userID, 10), "deleted") response.OK(c, gin.H{"userId": strconv.FormatInt(userID, 10)}) } func gameWhitelistUserFromProto(item *gamev1.GameWhitelistUser, profile *userclient.User) gameWhitelistUserDTO { if item == nil { return gameWhitelistUserDTO{} } dto := gameWhitelistUserDTO{ UserID: strconv.FormatInt(item.GetUserId(), 10), CreatedByAdminID: item.GetCreatedByAdminId(), CreatedAtMS: item.GetCreatedAtMs(), } if profile != nil { dto.DisplayUserID = profile.DisplayUserID dto.Username = profile.Username dto.Avatar = profile.Avatar dto.Status = profile.Status } return dto }