隐藏土耳其地区的游戏
This commit is contained in:
parent
cbe388b071
commit
4e3c9d01aa
@ -116,6 +116,10 @@ func main() {
|
||||
baishun.NewAppProvider(baishunService),
|
||||
lingxian.NewAppProvider(lingxianService),
|
||||
)
|
||||
gameVisibility := gameprovider.NewVisibilityPolicy(
|
||||
&app.Gateways,
|
||||
gameprovider.NewCachedIPCountryResolver(app.Repository.Redis, app.Config.HTTP.Timeout),
|
||||
)
|
||||
engine := router.NewRouter(app.Config, app.Repository, &app.Gateways, router.Services{
|
||||
AppPopup: appPopupService,
|
||||
ErrorLog: errorLogService,
|
||||
@ -126,6 +130,7 @@ func main() {
|
||||
Lingxian: lingxianService,
|
||||
GameOpen: gameOpenService,
|
||||
GameProviders: gameProviders,
|
||||
GameVisibility: gameVisibility,
|
||||
LuckyGift: luckyGiftService,
|
||||
ManagerCenter: managerCenterService,
|
||||
PropsStore: propsStoreService,
|
||||
|
||||
@ -6,4 +6,6 @@ type AuthUser struct {
|
||||
SysOrigin string
|
||||
Token string
|
||||
Authorization string
|
||||
RegionID string
|
||||
RegionCode string
|
||||
}
|
||||
|
||||
@ -146,6 +146,11 @@ func (g *Gateways) GetUserRegion(ctx context.Context, userID int64, authorizatio
|
||||
return g.Profile.GetUserRegion(ctx, userID, authorization)
|
||||
}
|
||||
|
||||
// GetUserLevel 透传到用户等级网关。
|
||||
func (g *Gateways) GetUserLevel(ctx context.Context, sysOrigin string, userID int64) (UserLevel, error) {
|
||||
return g.Profile.GetUserLevel(ctx, sysOrigin, userID)
|
||||
}
|
||||
|
||||
// ResolveRegionCodeByCountryCode 透传到 Java 区域配置网关。
|
||||
func (g *Gateways) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
||||
return g.Profile.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
|
||||
@ -400,6 +405,11 @@ func (g *ProfileGateway) GetUserRegion(ctx context.Context, userID int64, author
|
||||
return g.client.GetUserRegion(ctx, userID, authorization)
|
||||
}
|
||||
|
||||
// GetUserLevel 查询用户财富/魅力等级。
|
||||
func (g *ProfileGateway) GetUserLevel(ctx context.Context, sysOrigin string, userID int64) (UserLevel, error) {
|
||||
return g.client.GetUserLevel(ctx, sysOrigin, userID)
|
||||
}
|
||||
|
||||
// ResolveRegionCodeByCountryCode 根据 Java 区域配置把国家码解析成区域编码。
|
||||
func (g *ProfileGateway) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
||||
return g.client.ResolveRegionCodeByCountryCode(ctx, sysOrigin, countryCode)
|
||||
|
||||
@ -59,6 +59,12 @@ type UserRegion struct {
|
||||
RegionCode string `json:"regionCode"`
|
||||
}
|
||||
|
||||
type UserLevel struct {
|
||||
UserID Int64Value `json:"userId"`
|
||||
WealthLevel int `json:"wealthLevel"`
|
||||
CharmLevel int `json:"charmLevel"`
|
||||
}
|
||||
|
||||
type RegionConfig struct {
|
||||
RegionCode string `json:"regionCode"`
|
||||
SysOrigin string `json:"sysOrigin"`
|
||||
@ -1044,6 +1050,21 @@ func (c *Client) GetUserRegion(ctx context.Context, userID int64, authorization
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func (c *Client) GetUserLevel(ctx context.Context, sysOrigin string, userID int64) (UserLevel, error) {
|
||||
if userID <= 0 {
|
||||
return UserLevel{}, fmt.Errorf("userId is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("sysOrigin", strings.TrimSpace(sysOrigin))
|
||||
values.Set("userId", strconv.FormatInt(userID, 10))
|
||||
endpoint := c.cfg.Java.OtherBaseURL + "/user-level/client/getLevelByUserId?" + values.Encode()
|
||||
var resp resultResponse[UserLevel]
|
||||
if err := c.getJSON(ctx, endpoint, nil, &resp); err != nil {
|
||||
return UserLevel{}, err
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
func (c *Client) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
||||
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
|
||||
if countryCode == "" {
|
||||
|
||||
@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
// registerGameProviderRoutes 注册统一游戏厂商 app 路由。
|
||||
func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, registry *gameprovider.Registry) {
|
||||
func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, registry *gameprovider.Registry, visibility *gameprovider.VisibilityPolicy) {
|
||||
if registry == nil {
|
||||
return
|
||||
}
|
||||
@ -19,7 +19,15 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
appGroup := engine.Group("/app/game")
|
||||
appGroup.Use(authMiddleware(javaClient))
|
||||
appGroup.GET("/room/shortcut", func(c *gin.Context) {
|
||||
resp, err := registry.ListShortcutGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
||||
user, visible, ok := gameListAccess(c, visibility)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !visible {
|
||||
writeOK(c, []publicRoomGameItem{})
|
||||
return
|
||||
}
|
||||
resp, err := registry.ListShortcutGames(c.Request.Context(), user, c.Query("roomId"))
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
@ -27,7 +35,15 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
writeOK(c, publicRoomGameItems(resp))
|
||||
})
|
||||
appGroup.GET("/room/list", func(c *gin.Context) {
|
||||
resp, err := registry.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category"))
|
||||
user, visible, ok := gameListAccess(c, visibility)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !visible {
|
||||
writeOK(c, publicRoomGameListResponse(&gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}}))
|
||||
return
|
||||
}
|
||||
resp, err := registry.ListRoomGames(c.Request.Context(), user, c.Query("roomId"), c.Query("category"))
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
@ -72,14 +88,22 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
providerGroup := engine.Group("/app/game/providers")
|
||||
providerGroup.Use(authMiddleware(javaClient))
|
||||
providerGroup.GET("", func(c *gin.Context) {
|
||||
if gameprovider.IsEmptyGameListUser(mustAuthUser(c).UserID) {
|
||||
_, visible, ok := gameListAccess(c, visibility)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !visible {
|
||||
writeOK(c, gameprovider.ProviderListResponse{Items: []gameprovider.ProviderSummary{}})
|
||||
return
|
||||
}
|
||||
writeOK(c, registry.List())
|
||||
})
|
||||
providerGroup.GET("/:provider/room/shortcut", func(c *gin.Context) {
|
||||
if gameprovider.IsEmptyGameListUser(mustAuthUser(c).UserID) {
|
||||
user, visible, ok := gameListAccess(c, visibility)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !visible {
|
||||
writeOK(c, gin.H{"items": []publicRoomGameItem{}})
|
||||
return
|
||||
}
|
||||
@ -87,7 +111,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
resp, err := provider.ListShortcutGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"))
|
||||
resp, err := provider.ListShortcutGames(c.Request.Context(), user, c.Query("roomId"))
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
@ -95,7 +119,11 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
writeOK(c, gin.H{"items": publicRoomGameItems(resp)})
|
||||
})
|
||||
providerGroup.GET("/:provider/room/list", func(c *gin.Context) {
|
||||
if gameprovider.IsEmptyGameListUser(mustAuthUser(c).UserID) {
|
||||
user, visible, ok := gameListAccess(c, visibility)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !visible {
|
||||
writeOK(c, publicRoomGameListResponse(&gameprovider.RoomGameListResponse{Items: []gameprovider.RoomGameListItem{}}))
|
||||
return
|
||||
}
|
||||
@ -103,7 +131,7 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
resp, err := provider.ListRoomGames(c.Request.Context(), mustAuthUser(c), c.Query("roomId"), c.Query("category"))
|
||||
resp, err := provider.ListRoomGames(c.Request.Context(), user, c.Query("roomId"), c.Query("category"))
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return
|
||||
@ -158,6 +186,25 @@ func registerGameProviderRoutes(engine *gin.Engine, javaClient authGateway, regi
|
||||
})
|
||||
}
|
||||
|
||||
func gameListAccess(c *gin.Context, visibility *gameprovider.VisibilityPolicy) (common.AuthUser, bool, bool) {
|
||||
user := mustAuthUser(c)
|
||||
if gameprovider.IsEmptyGameListUser(user.UserID) {
|
||||
return user, false, true
|
||||
}
|
||||
if visibility == nil {
|
||||
return user, true, true
|
||||
}
|
||||
// 游戏列表只在列表入口做展示门禁,结果里的区域继续向下传给 provider SQL 做 regions 过滤。
|
||||
result, err := visibility.Evaluate(c.Request.Context(), user, resolveClientIP(c))
|
||||
if err != nil {
|
||||
writeError(c, err)
|
||||
return user, false, false
|
||||
}
|
||||
user.RegionID = result.RegionID
|
||||
user.RegionCode = result.RegionCode
|
||||
return user, result.Allow, true
|
||||
}
|
||||
|
||||
func resolveGameProvider(c *gin.Context, registry *gameprovider.Registry) (gameprovider.Provider, bool) {
|
||||
provider, err := registry.Resolve(c.Param("provider"))
|
||||
if err != nil {
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"chatapp3-golang/internal/common"
|
||||
"chatapp3-golang/internal/integration"
|
||||
"chatapp3-golang/internal/service/gameprovider"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func TestPublicRoomGameListResponseKeepsLegacyProviderFields(t *testing.T) {
|
||||
@ -141,3 +149,158 @@ func TestPublicLaunchMapsLingxianToLeaderForOldClients(t *testing.T) {
|
||||
t.Fatalf("roomState.provider = %q, want LEADER", resp.RoomState.Provider)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameProviderRoutesReturnEmptyWhenVisibilityPolicyDenies(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
provider := &routeStubProvider{}
|
||||
engine := gin.New()
|
||||
registerGameProviderRoutes(
|
||||
engine,
|
||||
routeStubAuthGateway{},
|
||||
gameprovider.NewRegistry(provider),
|
||||
gameprovider.NewVisibilityPolicy(
|
||||
routeStubVisibilityGateway{
|
||||
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "TR"},
|
||||
level: integration.UserLevel{WealthLevel: 1},
|
||||
targetRegionCode: "TR",
|
||||
},
|
||||
routeStubIPCountryResolver{countryCode: "TR"},
|
||||
),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/app/game/room/list?roomId=1", nil)
|
||||
req.Header.Set("Authorization", "Bearer token")
|
||||
rec := httptest.NewRecorder()
|
||||
engine.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
Body struct {
|
||||
Items []publicRoomGameItem `json:"items"`
|
||||
} `json:"body"`
|
||||
}
|
||||
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 provider.listRoomCalls != 0 {
|
||||
t.Fatalf("provider list calls = %d, want 0", provider.listRoomCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameProviderRoutesReturnProvidersWhenVisibilityPolicyAllows(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
engine := gin.New()
|
||||
registerGameProviderRoutes(
|
||||
engine,
|
||||
routeStubAuthGateway{},
|
||||
gameprovider.NewRegistry(&routeStubProvider{}),
|
||||
gameprovider.NewVisibilityPolicy(
|
||||
routeStubVisibilityGateway{
|
||||
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "TR"},
|
||||
level: integration.UserLevel{WealthLevel: 2},
|
||||
targetRegionCode: "TR",
|
||||
},
|
||||
routeStubIPCountryResolver{countryCode: "TR"},
|
||||
),
|
||||
)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/app/game/providers", nil)
|
||||
req.Header.Set("Authorization", "Bearer token")
|
||||
req.Header.Set("X-Real-IP", "88.255.1.1")
|
||||
rec := httptest.NewRecorder()
|
||||
engine.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
var payload struct {
|
||||
Body struct {
|
||||
Items []gameprovider.ProviderSummary `json:"items"`
|
||||
} `json:"body"`
|
||||
}
|
||||
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) != 1 {
|
||||
t.Fatalf("providers = %d, want 1", len(payload.Body.Items))
|
||||
}
|
||||
if payload.Body.Items[0].Key != "TEST" {
|
||||
t.Fatalf("provider key = %q, want TEST", payload.Body.Items[0].Key)
|
||||
}
|
||||
}
|
||||
|
||||
type routeStubAuthGateway struct{}
|
||||
|
||||
func (routeStubAuthGateway) AuthenticateToken(context.Context, string) (integration.UserCredential, error) {
|
||||
return integration.UserCredential{UserID: integration.Int64Value(1001), SysOrigin: "LIKEI"}, nil
|
||||
}
|
||||
|
||||
func (routeStubAuthGateway) AuthenticateConsoleToken(context.Context, string) (integration.ConsoleAccount, error) {
|
||||
return integration.ConsoleAccount{}, nil
|
||||
}
|
||||
|
||||
type routeStubVisibilityGateway struct {
|
||||
region integration.UserRegion
|
||||
level integration.UserLevel
|
||||
targetRegionCode string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (p *routeStubProvider) Key() string { return "TEST" }
|
||||
|
||||
func (p *routeStubProvider) DisplayName() string { return "Test Provider" }
|
||||
|
||||
func (p *routeStubProvider) SupportsLaunch(context.Context, gameprovider.AuthUser, gameprovider.LaunchRequest) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (p *routeStubProvider) ListShortcutGames(context.Context, gameprovider.AuthUser, string) ([]gameprovider.RoomGameListItem, error) {
|
||||
return []gameprovider.RoomGameListItem{{ID: 1, GameID: "test_1", Provider: "TEST", ProviderGameID: "1", Name: "Test Game"}}, nil
|
||||
}
|
||||
|
||||
func (p *routeStubProvider) ListRoomGames(context.Context, gameprovider.AuthUser, string, string) (*gameprovider.RoomGameListResponse, error) {
|
||||
p.listRoomCalls++
|
||||
return &gameprovider.RoomGameListResponse{
|
||||
Items: []gameprovider.RoomGameListItem{{ID: 1, GameID: "test_1", Provider: "TEST", ProviderGameID: "1", Name: "Test Game"}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *routeStubProvider) GetRoomState(context.Context, gameprovider.AuthUser, string) (*gameprovider.RoomStateResponse, error) {
|
||||
return &gameprovider.RoomStateResponse{}, nil
|
||||
}
|
||||
|
||||
func (p *routeStubProvider) LaunchGame(context.Context, gameprovider.AuthUser, gameprovider.LaunchRequest, string) (*gameprovider.LaunchResponse, error) {
|
||||
return nil, common.NewAppError(http.StatusBadRequest, "unsupported", "unsupported")
|
||||
}
|
||||
|
||||
func (p *routeStubProvider) CloseGame(context.Context, gameprovider.AuthUser, gameprovider.CloseRequest) (*gameprovider.RoomStateResponse, error) {
|
||||
return &gameprovider.RoomStateResponse{}, nil
|
||||
}
|
||||
|
||||
@ -112,6 +112,9 @@ func trimBearer(authorization string) string {
|
||||
|
||||
// resolveClientIP 解析请求来源 IP。
|
||||
func resolveClientIP(c *gin.Context) string {
|
||||
if realIP := strings.TrimSpace(c.GetHeader("X-Real-IP")); realIP != "" {
|
||||
return realIP
|
||||
}
|
||||
if forwarded := strings.TrimSpace(c.GetHeader("X-Forwarded-For")); forwarded != "" {
|
||||
parts := strings.Split(forwarded, ",")
|
||||
if len(parts) > 0 {
|
||||
|
||||
@ -48,6 +48,7 @@ type Services struct {
|
||||
Lingxian *lingxian.Service
|
||||
GameOpen *gameopen.GameOpenService
|
||||
GameProviders *gameprovider.Registry
|
||||
GameVisibility *gameprovider.VisibilityPolicy
|
||||
LuckyGift *luckygift.LuckyGiftService
|
||||
ManagerCenter *managercenter.Service
|
||||
PropsStore *propsstore.Service
|
||||
@ -100,7 +101,7 @@ func NewRouter(
|
||||
registerRoomTurnoverRewardRoutes(engine, javaClient, services.RoomTurnoverReward)
|
||||
registerHostCenterRoutes(engine, services.HostCenter)
|
||||
registerGameOpenRoutes(engine, cfg, services.GameOpen)
|
||||
registerGameProviderRoutes(engine, javaClient, services.GameProviders)
|
||||
registerGameProviderRoutes(engine, javaClient, services.GameProviders, services.GameVisibility)
|
||||
registerBaishunRoutes(engine, cfg, javaClient, services.Baishun)
|
||||
registerLingxianRoutes(engine, javaClient, services.Lingxian)
|
||||
registerLuckyGiftRoutes(engine, cfg, services.LuckyGift)
|
||||
|
||||
@ -13,7 +13,7 @@ import (
|
||||
|
||||
// ListShortcutGames 返回房间里的快捷游戏列表,数量上限为 5 个。
|
||||
func (s *BaishunService) ListShortcutGames(ctx context.Context, user AuthUser, roomID string) ([]RoomGameListItem, error) {
|
||||
items, err := s.listRoomGames(ctx, user.SysOrigin, roomID, "")
|
||||
items, err := s.listRoomGames(ctx, user.SysOrigin, user.RegionID, roomID, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -25,7 +25,7 @@ func (s *BaishunService) ListShortcutGames(ctx context.Context, user AuthUser, r
|
||||
|
||||
// ListRoomGames 返回房间可启动的完整游戏列表。
|
||||
func (s *BaishunService) ListRoomGames(ctx context.Context, user AuthUser, roomID string, category string) (*RoomGameListResponse, error) {
|
||||
items, err := s.listRoomGames(ctx, user.SysOrigin, roomID, category)
|
||||
items, err := s.listRoomGames(ctx, user.SysOrigin, user.RegionID, roomID, category)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -59,7 +59,7 @@ func (s *BaishunService) GetRoomState(ctx context.Context, user AuthUser, roomID
|
||||
}
|
||||
|
||||
// listRoomGames 联表读取房间可启动游戏,并按展示规则排序。
|
||||
func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, category string) ([]RoomGameListItem, error) {
|
||||
func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, regionID, roomID, category string) ([]RoomGameListItem, error) {
|
||||
sysOrigin = normalizeAdminSysOrigin(sysOrigin)
|
||||
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||
if err != nil {
|
||||
@ -102,6 +102,10 @@ func (s *BaishunService) listRoomGames(ctx context.Context, sysOrigin, roomID, c
|
||||
Joins("JOIN sys_game_list_vendor_ext ext ON ext.game_list_config_id = cfg.id AND ext.enabled = 1 AND ext.profile = ?", profile).
|
||||
Joins("LEFT JOIN baishun_game_catalog cat ON cat.sys_origin = cfg.sys_origin AND cat.profile = ext.profile AND CAST(cat.vendor_game_id AS CHAR) = ext.vendor_game_id").
|
||||
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, baishunVendorType)
|
||||
if strings.TrimSpace(regionID) != "" {
|
||||
// regions 为空表示全区域;有值时按用户区域 ID 精确匹配,避免土耳其用户看到其他区域游戏。
|
||||
query = query.Where("(COALESCE(TRIM(cfg.regions), '') = '' OR FIND_IN_SET(?, REPLACE(COALESCE(cfg.regions, ''), ' ', '')) > 0)", strings.TrimSpace(regionID))
|
||||
}
|
||||
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), "CHAT_ROOM") {
|
||||
query = query.Where("cfg.category = ?", category)
|
||||
}
|
||||
|
||||
250
internal/service/gameprovider/visibility.go
Normal file
250
internal/service/gameprovider/visibility.go
Normal file
@ -0,0 +1,250 @@
|
||||
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"
|
||||
gameListTargetCountryCode = "TR"
|
||||
gameListMinWealthLevel = 2
|
||||
ipCountryCachePrefix = "GAME_LIST:IP_COUNTRY:"
|
||||
ipCountryCacheTTL = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// 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 过滤。
|
||||
type VisibilityResult struct {
|
||||
Allow bool
|
||||
RegionID string
|
||||
RegionCode string
|
||||
}
|
||||
|
||||
// VisibilityPolicy 判断当前用户是否可以看到游戏列表。
|
||||
type VisibilityPolicy struct {
|
||||
java VisibilityGateway
|
||||
ipResolver IPCountryResolver
|
||||
targetCountry string
|
||||
targetSys string
|
||||
minWealth int
|
||||
}
|
||||
|
||||
// NewVisibilityPolicy 创建 LIKEI 游戏列表门禁:土耳其区域、土耳其 IP、财富等级 2 级及以上。
|
||||
func NewVisibilityPolicy(java VisibilityGateway, ipResolver IPCountryResolver) *VisibilityPolicy {
|
||||
return &VisibilityPolicy{
|
||||
java: java,
|
||||
ipResolver: ipResolver,
|
||||
targetCountry: gameListTargetCountryCode,
|
||||
targetSys: gameListTargetSysOrigin,
|
||||
minWealth: gameListMinWealthLevel,
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate 串行校验区域、财富等级和 IP 国家;任何条件不能确认时都按不可见处理。
|
||||
func (p *VisibilityPolicy) Evaluate(ctx context.Context, user AuthUser, clientIP string) (VisibilityResult, error) {
|
||||
if p == nil || !strings.EqualFold(strings.TrimSpace(user.SysOrigin), p.targetSys) {
|
||||
return VisibilityResult{Allow: true, RegionID: user.RegionID, RegionCode: user.RegionCode}, nil
|
||||
}
|
||||
if user.UserID <= 0 || p.java == nil {
|
||||
return VisibilityResult{}, nil
|
||||
}
|
||||
|
||||
region, err := p.java.GetUserRegion(ctx, user.UserID, user.Authorization)
|
||||
if err != nil {
|
||||
return VisibilityResult{}, err
|
||||
}
|
||||
result := VisibilityResult{
|
||||
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
|
||||
}
|
||||
if !matchesTargetRegion(result.RegionCode, targetRegionCode, p.targetCountry) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
level, err := p.java.GetUserLevel(ctx, user.SysOrigin, user.UserID)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if level.WealthLevel < p.minWealth {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if p.ipResolver == nil {
|
||||
return result, nil
|
||||
}
|
||||
countryCode, err := p.ipResolver.CountryCode(ctx, clientIP)
|
||||
if err != nil || !strings.EqualFold(strings.TrimSpace(countryCode), p.targetCountry) {
|
||||
return result, nil
|
||||
}
|
||||
result.Allow = true
|
||||
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))
|
||||
}
|
||||
132
internal/service/gameprovider/visibility_test.go
Normal file
132
internal/service/gameprovider/visibility_test.go
Normal file
@ -0,0 +1,132 @@
|
||||
package gameprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"chatapp3-golang/internal/integration"
|
||||
)
|
||||
|
||||
type stubVisibilityGateway struct {
|
||||
region integration.UserRegion
|
||||
level integration.UserLevel
|
||||
targetRegionCode string
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func TestVisibilityPolicyAllowsTurkeyRegionTurkeyIPAndWealthLevel2(t *testing.T) {
|
||||
policy := NewVisibilityPolicy(
|
||||
stubVisibilityGateway{
|
||||
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "TR"},
|
||||
level: integration.UserLevel{WealthLevel: 2},
|
||||
targetRegionCode: "TR",
|
||||
},
|
||||
stubIPCountryResolver{countryCode: "TR"},
|
||||
)
|
||||
|
||||
result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 1001, SysOrigin: "LIKEI"}, "1.1.1.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Evaluate() error = %v", err)
|
||||
}
|
||||
if !result.Allow {
|
||||
t.Fatalf("Evaluate() allow = false, want true")
|
||||
}
|
||||
if result.RegionID != "2046066409959649281" || result.RegionCode != "TR" {
|
||||
t.Fatalf("Evaluate() region = %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisibilityPolicyDeniesNonTurkeyIP(t *testing.T) {
|
||||
policy := NewVisibilityPolicy(
|
||||
stubVisibilityGateway{
|
||||
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "TR"},
|
||||
level: integration.UserLevel{WealthLevel: 2},
|
||||
targetRegionCode: "TR",
|
||||
},
|
||||
stubIPCountryResolver{countryCode: "SA"},
|
||||
)
|
||||
|
||||
result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 1001, SysOrigin: "LIKEI"}, "1.1.1.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Evaluate() error = %v", err)
|
||||
}
|
||||
if result.Allow {
|
||||
t.Fatalf("Evaluate() allow = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisibilityPolicyDeniesLowWealthLevel(t *testing.T) {
|
||||
policy := NewVisibilityPolicy(
|
||||
stubVisibilityGateway{
|
||||
region: integration.UserRegion{RegionID: "2046066409959649281", RegionCode: "TR"},
|
||||
level: integration.UserLevel{WealthLevel: 1},
|
||||
targetRegionCode: "TR",
|
||||
},
|
||||
stubIPCountryResolver{countryCode: "TR"},
|
||||
)
|
||||
|
||||
result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 1001, SysOrigin: "LIKEI"}, "1.1.1.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Evaluate() error = %v", err)
|
||||
}
|
||||
if result.Allow {
|
||||
t.Fatalf("Evaluate() allow = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisibilityPolicyDeniesNonTurkeyRegion(t *testing.T) {
|
||||
policy := NewVisibilityPolicy(
|
||||
stubVisibilityGateway{
|
||||
region: integration.UserRegion{RegionID: "2046067036517363714", RegionCode: "AR"},
|
||||
level: integration.UserLevel{WealthLevel: 2},
|
||||
targetRegionCode: "TR",
|
||||
},
|
||||
stubIPCountryResolver{countryCode: "TR"},
|
||||
)
|
||||
|
||||
result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 1001, SysOrigin: "LIKEI"}, "1.1.1.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Evaluate() error = %v", err)
|
||||
}
|
||||
if result.Allow {
|
||||
t.Fatalf("Evaluate() allow = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
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"},
|
||||
)
|
||||
|
||||
result, err := policy.Evaluate(context.Background(), AuthUser{UserID: 1001, SysOrigin: "ASWAT"}, "1.1.1.1")
|
||||
if err != nil {
|
||||
t.Fatalf("Evaluate() error = %v", err)
|
||||
}
|
||||
if !result.Allow {
|
||||
t.Fatalf("Evaluate() allow = false, want true")
|
||||
}
|
||||
}
|
||||
@ -47,7 +47,7 @@ func (p *AppProvider) ListShortcutGames(ctx context.Context, user gameprovider.A
|
||||
}
|
||||
|
||||
func (p *AppProvider) ListRoomGames(ctx context.Context, user gameprovider.AuthUser, roomID, category string) (*gameprovider.RoomGameListResponse, error) {
|
||||
items, err := p.service.listRoomGames(ctx, user.SysOrigin, category)
|
||||
items, err := p.service.listRoomGames(ctx, user.SysOrigin, user.RegionID, category)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -116,7 +116,7 @@ func (p *AppProvider) CloseGame(ctx context.Context, user gameprovider.AuthUser,
|
||||
return &gameprovider.RoomStateResponse{RoomID: req.RoomID, State: "IDLE", Provider: vendorType}, nil
|
||||
}
|
||||
|
||||
func (s *Service) listRoomGames(ctx context.Context, sysOrigin, category string) ([]gameprovider.RoomGameListItem, error) {
|
||||
func (s *Service) listRoomGames(ctx context.Context, sysOrigin, regionID, category string) ([]gameprovider.RoomGameListItem, error) {
|
||||
sysOrigin = normalizeSysOrigin(sysOrigin)
|
||||
profile, err := s.activeProfile(ctx, sysOrigin)
|
||||
if err != nil {
|
||||
@ -125,6 +125,10 @@ func (s *Service) listRoomGames(ctx context.Context, sysOrigin, category string)
|
||||
var rows []gameRow
|
||||
query := s.baseGameQuery(ctx, sysOrigin, profile).
|
||||
Where("cfg.sys_origin = ? AND cfg.is_showcase = 1 AND ext.vendor_type = ?", sysOrigin, vendorType)
|
||||
if strings.TrimSpace(regionID) != "" {
|
||||
// regions 为空表示全区域;有值时按用户区域 ID 精确匹配,避免土耳其用户看到其他区域游戏。
|
||||
query = query.Where("(COALESCE(TRIM(cfg.regions), '') = '' OR FIND_IN_SET(?, REPLACE(COALESCE(cfg.regions, ''), ' ', '')) > 0)", strings.TrimSpace(regionID))
|
||||
}
|
||||
if strings.TrimSpace(category) != "" && !strings.EqualFold(strings.TrimSpace(category), roomCategory) {
|
||||
query = query.Where("cfg.category = ?", category)
|
||||
}
|
||||
|
||||
17
migrations/051_game_list_turkey_region.sql
Normal file
17
migrations/051_game_list_turkey_region.sql
Normal file
@ -0,0 +1,17 @@
|
||||
-- Limit LIKEI app game-list rows to the Turkey region.
|
||||
-- Production Turkey Region observed in sys_region_config: 2046066409959649281.
|
||||
-- Re-check sys_region_config before production execution if region data has been rebuilt.
|
||||
-- After direct SQL execution, clear Redis GAME_LIST* cache or wait for its 1-hour TTL.
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
SET @sys_origin := 'LIKEI';
|
||||
SET @turkey_region_id := '2046066409959649281';
|
||||
|
||||
UPDATE sys_game_list_config
|
||||
SET regions = @turkey_region_id,
|
||||
update_time = NOW(3)
|
||||
WHERE sys_origin = @sys_origin
|
||||
AND is_showcase = 1;
|
||||
|
||||
COMMIT;
|
||||
Loading…
x
Reference in New Issue
Block a user