支持游戏白名单按短号匹配用户
This commit is contained in:
parent
3210db25c4
commit
8a665c11f4
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user