diff --git a/server/admin/internal/modules/regionblock/service.go b/server/admin/internal/modules/regionblock/service.go index fad006c2..1fa62d3e 100644 --- a/server/admin/internal/modules/regionblock/service.go +++ b/server/admin/internal/modules/regionblock/service.go @@ -13,6 +13,9 @@ import ( "hyapp-admin-server/internal/appctx" "hyapp-admin-server/internal/integration/userclient" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) const ( @@ -20,6 +23,7 @@ const ( maxRegionBlockKeywordLength = 128 maxIPWhitelistItems = 500 maxUserWhitelistItems = 500 + maxUserWhitelistLookupLength = 64 ) var ( @@ -51,11 +55,12 @@ type IPWhitelistItem struct { } type UserWhitelistItem struct { - WhitelistID int64 `json:"whitelist_id"` - UserID int64 `json:"user_id,string"` - Enabled bool `json:"enabled"` - CreatedAtMS int64 `json:"created_at_ms"` - UpdatedAtMS int64 `json:"updated_at_ms"` + WhitelistID int64 `json:"whitelist_id"` + UserID int64 `json:"user_id,string"` + DisplayUserID string `json:"display_user_id"` + Enabled bool `json:"enabled"` + CreatedAtMS int64 `json:"created_at_ms"` + UpdatedAtMS int64 `json:"updated_at_ms"` } type Config struct { @@ -121,7 +126,7 @@ func (s *Service) Replace(ctx context.Context, requestID string, actorID int64, } var normalizedUserIDs []int64 if whitelistUserIDs != nil { - normalizedUserIDs, err = normalizeUserIDWhitelist(*whitelistUserIDs) + normalizedUserIDs, err = s.normalizeUserIDWhitelist(ctx, requestID, *whitelistUserIDs) if err != nil { return Config{}, err } @@ -265,7 +270,7 @@ func normalizeIPWhitelist(raw []string) ([]string, error) { return result, nil } -func normalizeUserIDWhitelist(raw []string) ([]int64, error) { +func (s *Service) normalizeUserIDWhitelist(ctx context.Context, requestID string, raw []string) ([]int64, error) { if len(raw) > maxUserWhitelistItems { return nil, errInvalidPayload } @@ -276,7 +281,7 @@ func normalizeUserIDWhitelist(raw []string) ([]int64, error) { if value == "" { return nil, errInvalidPayload } - userID, err := strconv.ParseInt(value, 10, 64) + userID, err := s.resolveWhitelistUserID(ctx, requestID, value) if err != nil || userID <= 0 { return nil, errInvalidPayload } @@ -289,6 +294,75 @@ func normalizeUserIDWhitelist(raw []string) ([]int64, error) { return result, nil } +func (s *Service) resolveWhitelistUserID(ctx context.Context, requestID string, raw string) (int64, error) { + value := strings.TrimSpace(raw) + if value == "" || utf8.RuneCountInString(value) > maxUserWhitelistLookupLength { + return 0, errInvalidPayload + } + if s.user == nil { + // 本地隔离测试没有 user-service client 时只接受真实 user_id,避免把短号误写成未知用户。 + numericID, numericOK := parsePositiveInt64(value) + if !numericOK { + return 0, errInvalidPayload + } + return numericID, nil + } + + // 后台输入优先按当前 active 展示号解析,默认短号和靓号都由 user-service 统一处理; + // 保存时只落真实 user_id,后续用户改短号或靓号过期也不会影响登录风控白名单。 + if identity, err := s.user.ResolveDisplayUserID(ctx, userclient.ResolveDisplayUserIDRequest{ + RequestID: requestID, + Caller: "admin-server", + DisplayUserID: value, + }); err != nil { + if !isGRPCNotFound(err) && !isGRPCInvalidArgument(err) { + return 0, err + } + } else if identity != nil && identity.UserID > 0 { + return identity.UserID, nil + } + + // 展示号没有命中时,纯数字输入再兜底按内部 user_id 校验;非数字靓号未命中就直接视为无效配置。 + numericID, numericOK := parsePositiveInt64(value) + if !numericOK { + return 0, errInvalidPayload + } + if found, err := s.userExists(ctx, requestID, numericID); err != nil { + return 0, err + } else if found { + return numericID, nil + } + return 0, errInvalidPayload +} + +func (s *Service) userExists(ctx context.Context, requestID string, userID int64) (bool, error) { + user, err := s.user.GetUser(ctx, userclient.GetUserRequest{ + RequestID: requestID, + Caller: "admin-server", + UserID: userID, + }) + if err != nil { + if isGRPCNotFound(err) { + return false, nil + } + return false, err + } + return user != nil && user.UserID > 0, nil +} + +func parsePositiveInt64(text string) (int64, bool) { + parsed, err := strconv.ParseInt(strings.TrimSpace(text), 10, 64) + return parsed, err == nil && parsed > 0 +} + +func isGRPCNotFound(err error) bool { + return status.Code(err) == codes.NotFound +} + +func isGRPCInvalidArgument(err error) bool { + return status.Code(err) == codes.InvalidArgument +} + func (s *Service) refreshWhitelistCache(ctx context.Context, requestID string) error { if s.user == nil { return nil @@ -385,10 +459,13 @@ func queryEnabledIPWhitelist(ctx context.Context, db *sql.DB, appCode string) ([ func queryEnabledUserWhitelist(ctx context.Context, db *sql.DB, appCode string) ([]UserWhitelistItem, error) { rows, err := db.QueryContext(ctx, ` - SELECT whitelist_id, user_id, enabled, created_at_ms, updated_at_ms - FROM login_risk_user_whitelist - WHERE app_code = ? AND enabled = 1 - ORDER BY user_id ASC, whitelist_id ASC + SELECT w.whitelist_id, w.user_id, COALESCE(u.current_display_user_id, ''), w.enabled, w.created_at_ms, w.updated_at_ms + FROM login_risk_user_whitelist w + LEFT JOIN users u + ON u.app_code = w.app_code + AND u.user_id = w.user_id + WHERE w.app_code = ? AND w.enabled = 1 + ORDER BY w.user_id ASC, w.whitelist_id ASC `, appCode) if err != nil { return nil, err @@ -398,7 +475,7 @@ func queryEnabledUserWhitelist(ctx context.Context, db *sql.DB, appCode string) items := make([]UserWhitelistItem, 0) for rows.Next() { var item UserWhitelistItem - if err := rows.Scan(&item.WhitelistID, &item.UserID, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil { + if err := rows.Scan(&item.WhitelistID, &item.UserID, &item.DisplayUserID, &item.Enabled, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil { return nil, err } if item.UserID > 0 { diff --git a/server/admin/internal/modules/regionblock/service_test.go b/server/admin/internal/modules/regionblock/service_test.go index f8724114..2a1d101f 100644 --- a/server/admin/internal/modules/regionblock/service_test.go +++ b/server/admin/internal/modules/regionblock/service_test.go @@ -7,13 +7,18 @@ import ( "testing" "hyapp-admin-server/internal/integration/userclient" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) type fakeWhitelistCacheUserClient struct { userclient.Client - called bool - err error + called bool + err error + users map[int64]*userclient.User + identities map[string]*userclient.UserIdentity } func (f *fakeWhitelistCacheUserClient) RefreshLoginRiskWhitelistCache(_ context.Context, req userclient.RefreshLoginRiskWhitelistCacheRequest) (*userclient.LoginRiskWhitelistCacheSnapshot, error) { @@ -27,20 +32,50 @@ func (f *fakeWhitelistCacheUserClient) RefreshLoginRiskWhitelistCache(_ context. return &userclient.LoginRiskWhitelistCacheSnapshot{Refreshed: true, UserWhitelistCount: 2}, nil } -func TestNormalizeUserIDWhitelistDeduplicatesPositiveIDs(t *testing.T) { - got, err := normalizeUserIDWhitelist([]string{" 900025 ", "900026", "900025"}) +func (f *fakeWhitelistCacheUserClient) ResolveDisplayUserID(_ context.Context, req userclient.ResolveDisplayUserIDRequest) (*userclient.UserIdentity, error) { + if req.RequestID != "req-refresh" || req.Caller != "admin-server" { + return nil, errors.New("unexpected resolve request") + } + if identity := f.identities[req.DisplayUserID]; identity != nil { + return identity, nil + } + return nil, status.Error(codes.NotFound, "display user id not found") +} + +func (f *fakeWhitelistCacheUserClient) GetUser(_ context.Context, req userclient.GetUserRequest) (*userclient.User, error) { + if req.RequestID != "req-refresh" || req.Caller != "admin-server" { + return nil, errors.New("unexpected get user request") + } + if user := f.users[req.UserID]; user != nil { + return user, nil + } + return nil, status.Error(codes.NotFound, "user not found") +} + +func TestNormalizeUserIDWhitelistResolvesDisplayIDsAndInternalIDs(t *testing.T) { + service := NewService(nil, &fakeWhitelistCacheUserClient{ + users: map[int64]*userclient.User{ + 990000: {UserID: 990000}, + }, + identities: map[string]*userclient.UserIdentity{ + "VIP2026": {UserID: 900025, DisplayUserID: "VIP2026"}, + "123456789012": {UserID: 900026, DisplayUserID: "123456789012"}, + }, + }) + got, err := service.normalizeUserIDWhitelist(context.Background(), "req-refresh", []string{" VIP2026 ", "123456789012", "990000", "VIP2026"}) if err != nil { t.Fatalf("normalize user whitelist failed: %v", err) } - want := []int64{900025, 900026} + want := []int64{900025, 900026, 990000} if !reflect.DeepEqual(got, want) { t.Fatalf("normalized user whitelist = %+v, want %+v", got, want) } } func TestNormalizeUserIDWhitelistRejectsInvalidID(t *testing.T) { - for _, raw := range [][]string{{""}, {"0"}, {"-1"}, {"abc"}} { - if _, err := normalizeUserIDWhitelist(raw); !errors.Is(err, errInvalidPayload) { + service := NewService(nil, &fakeWhitelistCacheUserClient{}) + for _, raw := range [][]string{{""}, {"0"}, {"-1"}, {"VIP-2026"}, {"123456"}} { + if _, err := service.normalizeUserIDWhitelist(context.Background(), "req-refresh", raw); !errors.Is(err, errInvalidPayload) { t.Fatalf("normalize %v error = %v, want errInvalidPayload", raw, err) } }