支持游戏白名单按短号匹配用户

This commit is contained in:
zhx 2026-07-14 17:05:08 +08:00
parent 3210db25c4
commit 8a665c11f4
2 changed files with 174 additions and 11 deletions

View File

@ -1,6 +1,8 @@
package gamemanagement
import (
"context"
"errors"
"strconv"
"strings"
@ -10,17 +12,29 @@ import (
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 {
// 字符串承载 user_id避免浏览器 Number 对大整数发生精度损失
// 保留 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"`
@ -85,7 +99,7 @@ func (h *Handler) ListGameWhitelistUsers(c *gin.Context) {
response.OK(c, gin.H{"items": items, "serverTimeMs": resp.GetServerTimeMs()})
}
// AddGameWhitelistUser 先按内部 user_id 读取 owner 主数据,只有真实存在的用户才写入游戏访问成员表。
// AddGameWhitelistUser 先把内部 ID、默认短号或当前有效靓号统一解析为 owner user_id再写游戏访问成员表。
func (h *Handler) AddGameWhitelistUser(c *gin.Context) {
gameID := strings.TrimSpace(c.Param("game_id"))
var req gameWhitelistUserRequest
@ -93,29 +107,86 @@ func (h *Handler) AddGameWhitelistUser(c *gin.Context) {
response.BadRequest(c, "游戏白名单用户参数不正确")
return
}
userID, err := strconv.ParseInt(strings.TrimSpace(req.UserID), 10, 64)
if err != nil || userID <= 0 {
response.BadRequest(c, "用户 ID 参数不正确")
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
}
profile, err := h.user.GetUser(c.Request.Context(), userclient.GetUserRequest{
RequestID: middleware.CurrentRequestID(c), Caller: "admin-server", UserID: userID,
})
if err != nil || profile == nil {
response.BadRequest(c, "用户不存在")
response.ServerError(c, "匹配用户失败")
return
}
resp, err := h.game.AddGameWhitelistUser(c.Request.Context(), &gamev1.AddGameWhitelistUserRequest{
Meta: requestMeta(c), GameId: gameID, UserId: userID,
Meta: requestMeta(c), GameId: gameID, UserId: profile.UserID,
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
h.auditLog(c, "add-game-whitelist-user", "game_user_whitelist", gameID+":"+req.UserID, "added")
// 审计只记录最终内部 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)

View File

@ -0,0 +1,92 @@
package gamemanagement
import (
"context"
"errors"
"reflect"
"strconv"
"testing"
"hyapp-admin-server/internal/integration/userclient"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type fakeGameWhitelistUserResolver struct {
users map[int64]*userclient.User
identities map[string]*userclient.UserIdentity
calls []string
}
func (f *fakeGameWhitelistUserResolver) GetUser(_ context.Context, req userclient.GetUserRequest) (*userclient.User, error) {
f.calls = append(f.calls, "get:"+strconv.FormatInt(req.UserID, 10))
if profile := f.users[req.UserID]; profile != nil {
return profile, nil
}
return nil, status.Error(codes.NotFound, "user not found")
}
func (f *fakeGameWhitelistUserResolver) ResolveDisplayUserID(_ context.Context, req userclient.ResolveDisplayUserIDRequest) (*userclient.UserIdentity, error) {
f.calls = append(f.calls, "resolve:"+req.DisplayUserID)
if identity := f.identities[req.DisplayUserID]; identity != nil {
return identity, nil
}
return nil, status.Error(codes.NotFound, "display_user_id not found")
}
func TestResolveGameWhitelistUserAcceptsShortAndPrettyDisplayIDs(t *testing.T) {
for _, keyword := range []string{"163001", "VIP2026"} {
t.Run(keyword, func(t *testing.T) {
resolver := &fakeGameWhitelistUserResolver{
users: map[int64]*userclient.User{88: {UserID: 88, DisplayUserID: keyword, Username: "matched"}},
identities: map[string]*userclient.UserIdentity{keyword: {UserID: 88, DisplayUserID: keyword}},
}
profile, kind, err := resolveGameWhitelistUser(context.Background(), resolver, "req", keyword)
if err != nil || profile == nil || profile.UserID != 88 || kind != "display_user_id" {
t.Fatalf("resolve %q = profile=%+v kind=%q err=%v", keyword, profile, kind, err)
}
if want := []string{"resolve:" + keyword, "get:88"}; !reflect.DeepEqual(resolver.calls, want) {
t.Fatalf("calls = %v, want %v", resolver.calls, want)
}
})
}
}
func TestResolveGameWhitelistUserKeepsLongInternalIDAndFallsBackForLongPrettyID(t *testing.T) {
const internalID = int64(312899006709637120)
resolver := &fakeGameWhitelistUserResolver{
users: map[int64]*userclient.User{internalID: {UserID: internalID}},
identities: map[string]*userclient.UserIdentity{},
}
profile, kind, err := resolveGameWhitelistUser(context.Background(), resolver, "req", strconv.FormatInt(internalID, 10))
if err != nil || profile.UserID != internalID || kind != "user_id" {
t.Fatalf("internal ID resolve = profile=%+v kind=%q err=%v", profile, kind, err)
}
if want := []string{"get:312899006709637120"}; !reflect.DeepEqual(resolver.calls, want) {
t.Fatalf("calls = %v, want %v", resolver.calls, want)
}
resolver = &fakeGameWhitelistUserResolver{
users: map[int64]*userclient.User{77: {UserID: 77, DisplayUserID: "312899006709637120"}},
identities: map[string]*userclient.UserIdentity{
"312899006709637120": {UserID: 77, DisplayUserID: "312899006709637120"},
},
}
profile, kind, err = resolveGameWhitelistUser(context.Background(), resolver, "req", "312899006709637120")
if err != nil || profile.UserID != 77 || kind != "display_user_id" {
t.Fatalf("long pretty ID resolve = profile=%+v kind=%q err=%v", profile, kind, err)
}
wantCalls := []string{"get:312899006709637120", "resolve:312899006709637120", "get:77"}
if !reflect.DeepEqual(resolver.calls, wantCalls) {
t.Fatalf("calls = %v, want %v", resolver.calls, wantCalls)
}
}
func TestResolveGameWhitelistUserReportsMissingTarget(t *testing.T) {
resolver := &fakeGameWhitelistUserResolver{users: map[int64]*userclient.User{}, identities: map[string]*userclient.UserIdentity{}}
_, _, err := resolveGameWhitelistUser(context.Background(), resolver, "req", "VIP404")
if !errors.Is(err, errGameWhitelistUserNotFound) {
t.Fatalf("error = %v, want errGameWhitelistUserNotFound", err)
}
}