diff --git a/cmd/api/main.go b/cmd/api/main.go index 94f8fed..b495416 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -119,10 +119,7 @@ func main() { lingxian.NewAppProvider(lingxianService), hotgame.NewAppProvider(hotgameService), ) - gameVisibility := gameprovider.NewVisibilityPolicy( - &app.Gateways, - gameprovider.NewCachedIPCountryResolver(app.Repository.Redis, app.Config.HTTP.Timeout), - ) + gameVisibility := gameprovider.NewVisibilityPolicy(&app.Gateways) engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{ AppPopup: appPopupService, ErrorLog: errorLogService, diff --git a/internal/router/game_provider_routes.go b/internal/router/game_provider_routes.go index 94e5831..a6e4163 100644 --- a/internal/router/game_provider_routes.go +++ b/internal/router/game_provider_routes.go @@ -200,7 +200,7 @@ func gameListAccess(c *gin.Context, visibility *gameprovider.VisibilityPolicy) ( return user, true, true } // 游戏列表只在列表入口做展示门禁,结果里的区域继续向下传给 provider SQL 做 regions 过滤。 - result, err := visibility.Evaluate(c.Request.Context(), user, resolveClientIP(c)) + result, err := visibility.Evaluate(c.Request.Context(), user) if err != nil { writeError(c, err) return user, false, false diff --git a/internal/router/game_provider_routes_test.go b/internal/router/game_provider_routes_test.go index f4dd305..583365d 100644 --- a/internal/router/game_provider_routes_test.go +++ b/internal/router/game_provider_routes_test.go @@ -186,7 +186,7 @@ func TestPublicLaunchMapsLingxianToLeaderForOldClients(t *testing.T) { } } -func TestGameProviderRoutesReturnEmptyWhenVisibilityPolicyDenies(t *testing.T) { +func TestGameProviderRoutesReturnRoomListForTurkeyRegionUser(t *testing.T) { gin.SetMode(gin.TestMode) provider := &routeStubProvider{} engine := gin.New() @@ -196,11 +196,8 @@ func TestGameProviderRoutesReturnEmptyWhenVisibilityPolicyDenies(t *testing.T) { gameprovider.NewRegistry(provider), gameprovider.NewVisibilityPolicy( routeStubVisibilityGateway{ - region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, - level: integration.UserLevel{WealthLevel: 2}, - targetRegionCode: "土耳其", + region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, }, - routeStubIPCountryResolver{countryCode: "TR"}, ), ) @@ -220,11 +217,11 @@ func TestGameProviderRoutesReturnEmptyWhenVisibilityPolicyDenies(t *testing.T) { if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { t.Fatalf("response json error = %v: %s", err, rec.Body.String()) } - if len(payload.Body.Items) != 0 { - t.Fatalf("items = %d, want 0", len(payload.Body.Items)) + if len(payload.Body.Items) != 1 { + t.Fatalf("items = %d, want 1", len(payload.Body.Items)) } - if provider.listRoomCalls != 0 { - t.Fatalf("provider list calls = %d, want 0", provider.listRoomCalls) + if provider.listRoomCalls != 1 { + t.Fatalf("provider list calls = %d, want 1", provider.listRoomCalls) } } @@ -339,11 +336,8 @@ func TestGameProviderRoutesReturnProvidersWhenVisibilityPolicyAllows(t *testing. gameprovider.NewRegistry(&routeStubProvider{}), gameprovider.NewVisibilityPolicy( routeStubVisibilityGateway{ - region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, - level: integration.UserLevel{WealthLevel: 3}, - targetRegionCode: "土耳其", + region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, }, - routeStubIPCountryResolver{countryCode: "ZA"}, ), ) @@ -422,31 +416,13 @@ func (routeStubAuthGateway) AuthenticateConsoleToken(context.Context, string) (i } type routeStubVisibilityGateway struct { - region integration.UserRegion - level integration.UserLevel - targetRegionCode string + region integration.UserRegion } func (s routeStubVisibilityGateway) GetUserRegion(context.Context, int64, string) (integration.UserRegion, error) { return s.region, nil } -func (s routeStubVisibilityGateway) GetUserLevel(context.Context, string, int64) (integration.UserLevel, error) { - return s.level, nil -} - -func (s routeStubVisibilityGateway) ResolveRegionCodeByCountryCode(context.Context, string, string) (string, error) { - return s.targetRegionCode, nil -} - -type routeStubIPCountryResolver struct { - countryCode string -} - -func (s routeStubIPCountryResolver) CountryCode(context.Context, string) (string, error) { - return s.countryCode, nil -} - type routeStubProvider struct { listRoomCalls int shortcutCalls int diff --git a/internal/service/gameprovider/visibility.go b/internal/service/gameprovider/visibility.go index 016db80..5283989 100644 --- a/internal/service/gameprovider/visibility.go +++ b/internal/service/gameprovider/visibility.go @@ -2,38 +2,16 @@ package gameprovider import ( "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "net/url" "strings" - "time" "chatapp3-golang/internal/integration" - - "github.com/redis/go-redis/v9" ) -const ( - gameListTargetSysOrigin = "LIKEI" - gameListTurkeyCountryCode = "TR" - gameListTurkeyBlockLevel = 2 - ipCountryCachePrefix = "GAME_LIST:IP_COUNTRY:" - ipCountryCacheTTL = 7 * 24 * time.Hour -) +const gameListTargetSysOrigin = "LIKEI" -// VisibilityGateway 聚合游戏列表门禁需要的 Java 用户区域和等级能力。 +// VisibilityGateway 聚合游戏列表门禁需要的 Java 用户区域能力。 type VisibilityGateway interface { GetUserRegion(ctx context.Context, userID int64, authorization string) (integration.UserRegion, error) - GetUserLevel(ctx context.Context, sysOrigin string, userID int64) (integration.UserLevel, error) - ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) -} - -// IPCountryResolver 把请求 IP 解析成国家二字码。 -type IPCountryResolver interface { - CountryCode(ctx context.Context, ip string) (string, error) } // VisibilityResult 返回门禁结果,并把已经查到的区域带回路由继续参与 SQL 过滤。 @@ -45,26 +23,21 @@ type VisibilityResult struct { // VisibilityPolicy 判断当前用户是否可以看到游戏列表。 type VisibilityPolicy struct { - java VisibilityGateway - ipResolver IPCountryResolver - targetCountry string - targetSys string - blockLevel int + java VisibilityGateway + targetSys string } -// NewVisibilityPolicy 创建 LIKEI 游戏列表门禁:土耳其区域或土耳其 IP 的用户财富等级必须大于 2,其他用户直接展示。 -func NewVisibilityPolicy(java VisibilityGateway, ipResolver IPCountryResolver) *VisibilityPolicy { +// NewVisibilityPolicy 创建 LIKEI 游戏列表门禁:土耳其财富等级限制已下线,现在对所有区域直接展示, +// 保留 Evaluate 是因为路由仍依赖它查询用户区域给 provider SQL 做 regions 过滤。 +func NewVisibilityPolicy(java VisibilityGateway) *VisibilityPolicy { return &VisibilityPolicy{ - java: java, - ipResolver: ipResolver, - targetCountry: gameListTurkeyCountryCode, - targetSys: gameListTargetSysOrigin, - blockLevel: gameListTurkeyBlockLevel, + java: java, + targetSys: gameListTargetSysOrigin, } } -// Evaluate 只拦截土耳其区域或土耳其 IP 的低财富等级用户;非土耳其区域且非土耳其 IP 的用户直接展示游戏列表。 -func (p *VisibilityPolicy) Evaluate(ctx context.Context, user AuthUser, clientIP string) (VisibilityResult, error) { +// Evaluate 给 LIKEI 用户补齐区域信息并放行;非 LIKEI 用户沿用自带区域直接展示。 +func (p *VisibilityPolicy) Evaluate(ctx context.Context, user AuthUser) (VisibilityResult, error) { if p == nil || !strings.EqualFold(strings.TrimSpace(user.SysOrigin), p.targetSys) { return VisibilityResult{Allow: true, RegionID: user.RegionID, RegionCode: user.RegionCode}, nil } @@ -76,176 +49,9 @@ func (p *VisibilityPolicy) Evaluate(ctx context.Context, user AuthUser, clientIP if err != nil { return VisibilityResult{}, err } - result := VisibilityResult{ + return VisibilityResult{ + Allow: true, RegionID: strings.TrimSpace(region.RegionID), RegionCode: strings.TrimSpace(region.RegionCode), - } - - // 区域配置是运营可变数据,所以先从 Java 区域配置解析 TR 对应的 regionCode,再判断用户是否属于土耳其区域。 - targetRegionCode, err := p.java.ResolveRegionCodeByCountryCode(ctx, user.SysOrigin, p.targetCountry) - if err != nil { - return result, err - } - turkeyRegion := matchesTargetRegion(result.RegionCode, targetRegionCode, p.targetCountry) - turkeyIP := false - if p.ipResolver != nil { - countryCode, err := p.ipResolver.CountryCode(ctx, clientIP) - if err != nil { - return result, err - } - turkeyIP = strings.EqualFold(strings.TrimSpace(countryCode), p.targetCountry) - } - - // 用户区域和当前 IP 都不是土耳其时,直接放行;只有命中土耳其门禁时才需要读取财富等级。 - if !turkeyRegion && !turkeyIP { - result.Allow = true - return result, nil - } - - level, err := p.java.GetUserLevel(ctx, user.SysOrigin, user.UserID) - if err != nil { - return result, err - } - result.Allow = level.WealthLevel > p.blockLevel - return result, nil -} - -func matchesTargetRegion(userRegionCode, targetRegionCode, targetCountry string) bool { - userRegionCode = strings.TrimSpace(userRegionCode) - targetRegionCode = strings.TrimSpace(targetRegionCode) - if targetRegionCode != "" { - return strings.EqualFold(userRegionCode, targetRegionCode) - } - return strings.EqualFold(userRegionCode, strings.TrimSpace(targetCountry)) -} - -// CachedIPCountryResolver 使用 Redis 缓存 IP 国家码,未命中时按顺序调用几个公开 geoip provider。 -type CachedIPCountryResolver struct { - cache redis.Cmdable - httpClient *http.Client -} - -// NewCachedIPCountryResolver 创建可复用的 IP 国家解析器。 -func NewCachedIPCountryResolver(cache redis.Cmdable, timeout time.Duration) *CachedIPCountryResolver { - if timeout <= 0 { - timeout = 5 * time.Second - } - return &CachedIPCountryResolver{ - cache: cache, - httpClient: &http.Client{ - Timeout: timeout, - }, - } -} - -// CountryCode 返回 IP 对应国家二字码;解析不到时返回空字符串,调用方负责按不可见处理。 -func (r *CachedIPCountryResolver) CountryCode(ctx context.Context, ip string) (string, error) { - ip = strings.TrimSpace(ip) - if ip == "" || ip == "-" { - return "", nil - } - if cached := r.cachedCountryCode(ctx, ip); cached != "" { - return cached, nil - } - for _, provider := range ipGeoProviders { - countryCode := r.requestCountryCode(ctx, provider, ip) - if countryCode == "" { - continue - } - r.cacheCountryCode(ctx, ip, countryCode) - return countryCode, nil - } - return "", nil -} - -func (r *CachedIPCountryResolver) cachedCountryCode(ctx context.Context, ip string) string { - if r == nil || r.cache == nil { - return "" - } - value, err := r.cache.Get(ctx, ipCountryCachePrefix+ip).Result() - if err != nil && !errors.Is(err, redis.Nil) { - return "" - } - return strings.ToUpper(strings.TrimSpace(value)) -} - -func (r *CachedIPCountryResolver) cacheCountryCode(ctx context.Context, ip string, countryCode string) { - if r == nil || r.cache == nil || strings.TrimSpace(countryCode) == "" { - return - } - _ = r.cache.Set(ctx, ipCountryCachePrefix+ip, strings.ToUpper(strings.TrimSpace(countryCode)), ipCountryCacheTTL).Err() -} - -type ipGeoProvider struct { - name string - urlFormat string -} - -var ipGeoProviders = []ipGeoProvider{ - {name: "ip.sb", urlFormat: "https://api.ip.sb/geoip/%s"}, - {name: "freeipapi", urlFormat: "https://freeipapi.com/api/json/%s"}, - {name: "ipwhois", urlFormat: "https://ipwhois.app/json/%s?format=json"}, - {name: "ip.nc.gy", urlFormat: "https://ip.nc.gy/json?ip=%s"}, -} - -func (r *CachedIPCountryResolver) requestCountryCode(ctx context.Context, provider ipGeoProvider, ip string) string { - if r == nil || r.httpClient == nil { - return "" - } - endpoint := fmt.Sprintf(provider.urlFormat, url.QueryEscape(ip)) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) - if err != nil { - return "" - } - resp, err := r.httpClient.Do(req) - if err != nil { - return "" - } - defer resp.Body.Close() - if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { - return "" - } - body, err := io.ReadAll(resp.Body) - if err != nil || len(body) == 0 { - return "" - } - var payload map[string]any - if err := json.Unmarshal(body, &payload); err != nil { - return "" - } - return countryCodeFromProvider(provider.name, payload) -} - -func countryCodeFromProvider(provider string, payload map[string]any) string { - switch provider { - case "ip.sb": - return upperString(payload["country_code"]) - case "freeipapi": - return upperString(payload["countryCode"]) - case "ipwhois": - if success, ok := payload["success"].(bool); ok && !success { - return "" - } - return upperString(payload["country_code"]) - case "ip.nc.gy": - if code := countryObjectCode(payload["country"]); code != "" { - return code - } - return countryObjectCode(payload["registered_country"]) - default: - return "" - } -} - -func countryObjectCode(value any) string { - object, ok := value.(map[string]any) - if !ok { - return "" - } - return upperString(object["iso_code"]) -} - -func upperString(value any) string { - text, _ := value.(string) - return strings.ToUpper(strings.TrimSpace(text)) + }, nil } diff --git a/internal/service/gameprovider/visibility_test.go b/internal/service/gameprovider/visibility_test.go index 81b1719..e74ec8f 100644 --- a/internal/service/gameprovider/visibility_test.go +++ b/internal/service/gameprovider/visibility_test.go @@ -8,132 +8,73 @@ import ( ) type stubVisibilityGateway struct { - region integration.UserRegion - level integration.UserLevel - targetRegionCode string + region integration.UserRegion } func (s stubVisibilityGateway) GetUserRegion(context.Context, int64, string) (integration.UserRegion, error) { return s.region, nil } -func (s stubVisibilityGateway) GetUserLevel(context.Context, string, int64) (integration.UserLevel, error) { - return s.level, nil -} - -func (s stubVisibilityGateway) ResolveRegionCodeByCountryCode(context.Context, string, string) (string, error) { - return s.targetRegionCode, nil -} - -type stubIPCountryResolver struct { - countryCode string -} - -func (s stubIPCountryResolver) CountryCode(context.Context, string) (string, error) { - return s.countryCode, nil -} - const account1007500UserID int64 = 2054163533368717314 -func TestVisibilityPolicyAppliesTurkeyRegionOrIPWealthGate(t *testing.T) { +func TestVisibilityPolicyAllowsAllRegions(t *testing.T) { tests := []struct { - name string - region integration.UserRegion - ipCountry string - wealth int - wantAllow bool - wantRegion integration.UserRegion + name string + region integration.UserRegion }{ { - name: "turkey region level 2 is blocked", - region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, - ipCountry: "ZA", - wealth: 2, - wantAllow: false, - wantRegion: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, + name: "turkey region is visible without wealth gate", + region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, }, { - name: "turkey region level 3 is visible even when ip is not turkey", - region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, - ipCountry: "ZA", - wealth: 3, - wantAllow: true, - wantRegion: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "土耳其"}, - }, - { - name: "turkey ip level 2 is blocked", - region: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"}, - ipCountry: "TR", - wealth: 2, - wantAllow: false, - wantRegion: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"}, - }, - { - name: "turkey ip level 3 is visible", - region: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"}, - ipCountry: "TR", - wealth: 3, - wantAllow: true, - wantRegion: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"}, - }, - { - name: "non turkey region and non turkey ip are visible even at level 1", - region: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"}, - ipCountry: "ZA", - wealth: 1, - wantAllow: true, - wantRegion: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"}, - }, - { - name: "account 1007500 current region and ip are visible", - region: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"}, - ipCountry: "ZA", - wealth: 35, - wantAllow: true, - wantRegion: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"}, + name: "south asia region is visible", + region: integration.UserRegion{RegionID: "2045389832842178561", RegionCode: "南亚"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - policy := NewVisibilityPolicy( - stubVisibilityGateway{ - region: tt.region, - level: integration.UserLevel{WealthLevel: tt.wealth}, - targetRegionCode: "土耳其", - }, - stubIPCountryResolver{countryCode: tt.ipCountry}, - ) + policy := NewVisibilityPolicy(stubVisibilityGateway{region: tt.region}) - result, err := policy.Evaluate(context.Background(), AuthUser{UserID: account1007500UserID, SysOrigin: "LIKEI"}, "1.1.1.1") + result, err := policy.Evaluate(context.Background(), AuthUser{UserID: account1007500UserID, SysOrigin: "LIKEI"}) if err != nil { t.Fatalf("Evaluate() error = %v", err) } - if result.Allow != tt.wantAllow { - t.Fatalf("Evaluate() allow = %v, want %v", result.Allow, tt.wantAllow) + if !result.Allow { + t.Fatalf("Evaluate() allow = false, want true") } - if result.RegionID != tt.wantRegion.RegionID || result.RegionCode != tt.wantRegion.RegionCode { - t.Fatalf("Evaluate() region = %#v, want %#v", result, tt.wantRegion) + if result.RegionID != tt.region.RegionID || result.RegionCode != tt.region.RegionCode { + t.Fatalf("Evaluate() region = %#v, want %#v", result, tt.region) } }) } } func TestVisibilityPolicySkipsOtherSysOrigin(t *testing.T) { - policy := NewVisibilityPolicy( - stubVisibilityGateway{ - region: integration.UserRegion{RegionID: "2046067036517363714", RegionCode: "AR"}, - level: integration.UserLevel{WealthLevel: 0}, - targetRegionCode: "TR", - }, - stubIPCountryResolver{countryCode: "SA"}, - ) + policy := NewVisibilityPolicy(stubVisibilityGateway{ + region: integration.UserRegion{RegionID: "2046067036517363714", RegionCode: "AR"}, + }) - result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 1001, SysOrigin: "ASWAT"}, "1.1.1.1") + result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 1001, SysOrigin: "ASWAT", RegionID: "r-1", RegionCode: "SA"}) if err != nil { t.Fatalf("Evaluate() error = %v", err) } if !result.Allow { t.Fatalf("Evaluate() allow = false, want true") } + if result.RegionID != "r-1" || result.RegionCode != "SA" { + t.Fatalf("Evaluate() region = %#v, want user-provided region", result) + } +} + +func TestVisibilityPolicyDeniesInvalidUser(t *testing.T) { + policy := NewVisibilityPolicy(stubVisibilityGateway{}) + + result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 0, SysOrigin: "LIKEI"}) + if err != nil { + t.Fatalf("Evaluate() error = %v", err) + } + if result.Allow { + t.Fatalf("Evaluate() allow = true, want false for invalid user") + } }