From a9456239cc2f0e13bec79c5849972d11afccf259 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 2 Jun 2026 17:42:50 +0800 Subject: [PATCH] feat: persist game safe height --- api/proto/game/v1/game.pb.go | 40 +++++++++++++++--- api/proto/game/v1/game.proto | 3 ++ .../internal/modules/gamemanagement/dto.go | 2 + .../modules/gamemanagement/request.go | 2 + .../modules/gamemanagement/sync_games.go | 41 +++++++++++++++++++ .../modules/gamemanagement/sync_games_test.go | 5 ++- .../deploy/mysql/initdb/001_game_service.sql | 1 + .../game-service/internal/domain/game/game.go | 2 + .../internal/service/game/service.go | 2 + .../internal/storage/mysql/repository.go | 28 +++++++------ .../internal/transport/grpc/server.go | 7 ++++ .../transport/http/gameapi/game_view.go | 8 ++++ 12 files changed, 123 insertions(+), 18 deletions(-) diff --git a/api/proto/game/v1/game.pb.go b/api/proto/game/v1/game.pb.go index 94195959..d915bd38 100644 --- a/api/proto/game/v1/game.pb.go +++ b/api/proto/game/v1/game.pb.go @@ -265,6 +265,7 @@ type GameCatalogItem struct { Tags []string `protobuf:"bytes,14,rep,name=tags,proto3" json:"tags,omitempty"` CreatedAtMs int64 `protobuf:"varint,15,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` UpdatedAtMs int64 `protobuf:"varint,16,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` + SafeHeight int32 `protobuf:"varint,17,opt,name=safe_height,json=safeHeight,proto3" json:"safe_height,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -411,6 +412,13 @@ func (x *GameCatalogItem) GetUpdatedAtMs() int64 { return 0 } +func (x *GameCatalogItem) GetSafeHeight() int32 { + if x != nil { + return x.SafeHeight + } + return 0 +} + type AppGame struct { state protoimpl.MessageState `protogen:"open.v1"` GameId string `protobuf:"bytes,1,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"` @@ -426,6 +434,7 @@ type AppGame struct { Enabled bool `protobuf:"varint,11,opt,name=enabled,proto3" json:"enabled,omitempty"` Maintenance bool `protobuf:"varint,12,opt,name=maintenance,proto3" json:"maintenance,omitempty"` SortOrder int32 `protobuf:"varint,13,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + SafeHeight int32 `protobuf:"varint,14,opt,name=safe_height,json=safeHeight,proto3" json:"safe_height,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -551,6 +560,13 @@ func (x *AppGame) GetSortOrder() int32 { return 0 } +func (x *AppGame) GetSafeHeight() int32 { + if x != nil { + return x.SafeHeight + } + return 0 +} + type ListGamesRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` @@ -796,6 +812,7 @@ type LaunchGameResponse struct { ExpiresAtMs int64 `protobuf:"varint,3,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` Orientation string `protobuf:"bytes,4,opt,name=orientation,proto3" json:"orientation,omitempty"` ServerTimeMs int64 `protobuf:"varint,5,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + SafeHeight int32 `protobuf:"varint,6,opt,name=safe_height,json=safeHeight,proto3" json:"safe_height,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -865,6 +882,13 @@ func (x *LaunchGameResponse) GetServerTimeMs() int64 { return 0 } +func (x *LaunchGameResponse) GetSafeHeight() int32 { + if x != nil { + return x.SafeHeight + } + return 0 +} + type CallbackRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` @@ -1795,7 +1819,7 @@ const file_proto_game_v1_game_proto_rawDesc = "" + " \x01(\tR\x0ecallbackSecret\x12.\n" + "\x13callback_secret_set\x18\v \x01(\bR\x11callbackSecretSet\x122\n" + "\x15callback_ip_whitelist\x18\f \x03(\tR\x13callbackIpWhitelist\x12.\n" + - "\x13adapter_config_json\x18\r \x01(\tR\x11adapterConfigJson\"\xf6\x03\n" + + "\x13adapter_config_json\x18\r \x01(\tR\x11adapterConfigJson\"\x97\x04\n" + "\x0fGameCatalogItem\x12\x19\n" + "\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x17\n" + "\agame_id\x18\x02 \x01(\tR\x06gameId\x12#\n" + @@ -1815,7 +1839,9 @@ const file_proto_game_v1_game_proto_rawDesc = "" + "sort_order\x18\r \x01(\x05R\tsortOrder\x12\x12\n" + "\x04tags\x18\x0e \x03(\tR\x04tags\x12\"\n" + "\rcreated_at_ms\x18\x0f \x01(\x03R\vcreatedAtMs\x12\"\n" + - "\rupdated_at_ms\x18\x10 \x01(\x03R\vupdatedAtMs\"\x83\x03\n" + + "\rupdated_at_ms\x18\x10 \x01(\x03R\vupdatedAtMs\x12\x1f\n" + + "\vsafe_height\x18\x11 \x01(\x05R\n" + + "safeHeight\"\xa4\x03\n" + "\aAppGame\x12\x17\n" + "\agame_id\x18\x01 \x01(\tR\x06gameId\x12#\n" + "\rplatform_code\x18\x02 \x01(\tR\fplatformCode\x12\x19\n" + @@ -1832,7 +1858,9 @@ const file_proto_game_v1_game_proto_rawDesc = "" + "\aenabled\x18\v \x01(\bR\aenabled\x12 \n" + "\vmaintenance\x18\f \x01(\bR\vmaintenance\x12\x1d\n" + "\n" + - "sort_order\x18\r \x01(\x05R\tsortOrder\"\xec\x01\n" + + "sort_order\x18\r \x01(\x05R\tsortOrder\x12\x1f\n" + + "\vsafe_height\x18\x0e \x01(\x05R\n" + + "safeHeight\"\xec\x01\n" + "\x10ListGamesRequest\x12.\n" + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.game.v1.RequestMetaR\x04meta\x12\x17\n" + "\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x14\n" + @@ -1851,7 +1879,7 @@ const file_proto_game_v1_game_proto_rawDesc = "" + "\agame_id\x18\x04 \x01(\tR\x06gameId\x12\x14\n" + "\x05scene\x18\x05 \x01(\tR\x05scene\x12\x17\n" + "\aroom_id\x18\x06 \x01(\tR\x06roomId\x12!\n" + - "\faccess_token\x18\a \x01(\tR\vaccessToken\"\xbe\x01\n" + + "\faccess_token\x18\a \x01(\tR\vaccessToken\"\xdf\x01\n" + "\x12LaunchGameResponse\x12\x1d\n" + "\n" + "session_id\x18\x01 \x01(\tR\tsessionId\x12\x1d\n" + @@ -1859,7 +1887,9 @@ const file_proto_game_v1_game_proto_rawDesc = "" + "launch_url\x18\x02 \x01(\tR\tlaunchUrl\x12\"\n" + "\rexpires_at_ms\x18\x03 \x01(\x03R\vexpiresAtMs\x12 \n" + "\vorientation\x18\x04 \x01(\tR\vorientation\x12$\n" + - "\x0eserver_time_ms\x18\x05 \x01(\x03R\fserverTimeMs\"\xbe\x03\n" + + "\x0eserver_time_ms\x18\x05 \x01(\x03R\fserverTimeMs\x12\x1f\n" + + "\vsafe_height\x18\x06 \x01(\x05R\n" + + "safeHeight\"\xbe\x03\n" + "\x0fCallbackRequest\x12.\n" + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.game.v1.RequestMetaR\x04meta\x12#\n" + "\rplatform_code\x18\x02 \x01(\tR\fplatformCode\x12\x1c\n" + diff --git a/api/proto/game/v1/game.proto b/api/proto/game/v1/game.proto index 1e566ec7..3ab08aba 100644 --- a/api/proto/game/v1/game.proto +++ b/api/proto/game/v1/game.proto @@ -48,6 +48,7 @@ message GameCatalogItem { repeated string tags = 14; int64 created_at_ms = 15; int64 updated_at_ms = 16; + int32 safe_height = 17; } message AppGame { @@ -64,6 +65,7 @@ message AppGame { bool enabled = 11; bool maintenance = 12; int32 sort_order = 13; + int32 safe_height = 14; } message ListGamesRequest { @@ -99,6 +101,7 @@ message LaunchGameResponse { int64 expires_at_ms = 3; string orientation = 4; int64 server_time_ms = 5; + int32 safe_height = 6; } message CallbackRequest { diff --git a/server/admin/internal/modules/gamemanagement/dto.go b/server/admin/internal/modules/gamemanagement/dto.go index 872b903f..0579e4d3 100644 --- a/server/admin/internal/modules/gamemanagement/dto.go +++ b/server/admin/internal/modules/gamemanagement/dto.go @@ -34,6 +34,7 @@ type catalogDTO struct { LaunchURL string `json:"launchUrl,omitempty"` LaunchMode string `json:"launchMode"` Orientation string `json:"orientation"` + SafeHeight int32 `json:"safeHeight"` MinCoin int64 `json:"minCoin"` Status string `json:"status"` SortOrder int32 `json:"sortOrder"` @@ -78,6 +79,7 @@ func catalogFromProto(item *gamev1.GameCatalogItem) catalogDTO { CoverURL: item.GetCoverUrl(), LaunchMode: item.GetLaunchMode(), Orientation: item.GetOrientation(), + SafeHeight: item.GetSafeHeight(), MinCoin: item.GetMinCoin(), Status: item.GetStatus(), SortOrder: item.GetSortOrder(), diff --git a/server/admin/internal/modules/gamemanagement/request.go b/server/admin/internal/modules/gamemanagement/request.go index 9bf11103..41e31b49 100644 --- a/server/admin/internal/modules/gamemanagement/request.go +++ b/server/admin/internal/modules/gamemanagement/request.go @@ -36,6 +36,7 @@ type catalogRequest struct { CoverURL string `json:"coverUrl"` LaunchMode string `json:"launchMode"` Orientation string `json:"orientation"` + SafeHeight int32 `json:"safeHeight"` MinCoin int64 `json:"minCoin"` Status string `json:"status"` SortOrder int32 `json:"sortOrder"` @@ -77,6 +78,7 @@ func (r catalogRequest) toProto(gameID string) *gamev1.GameCatalogItem { CoverUrl: strings.TrimSpace(r.CoverURL), LaunchMode: strings.TrimSpace(r.LaunchMode), Orientation: strings.TrimSpace(r.Orientation), + SafeHeight: r.SafeHeight, MinCoin: r.MinCoin, Status: strings.TrimSpace(r.Status), SortOrder: r.SortOrder, diff --git a/server/admin/internal/modules/gamemanagement/sync_games.go b/server/admin/internal/modules/gamemanagement/sync_games.go index ff239266..98fb4d3c 100644 --- a/server/admin/internal/modules/gamemanagement/sync_games.go +++ b/server/admin/internal/modules/gamemanagement/sync_games.go @@ -478,6 +478,7 @@ type baishunGameListItem struct { CoverURL string `json:"cover_url"` DownloadURL string `json:"download_url"` GameOrientation any `json:"game_orientation"` + SafeHeight any `json:"safe_height"` } func baishunCatalogItems(platformCode string, providerItems []baishunGameListItem, req syncGamesRequest) ([]catalogRequest, map[string]string) { @@ -502,6 +503,7 @@ func baishunCatalogItems(platformCode string, providerItems []baishunGameListIte CoverURL: coverURL, LaunchMode: defaulted(req.LaunchMode, defaultLaunchMode), Orientation: baishunOrientation(providerItem.GameOrientation), + SafeHeight: int32FromJSONValue(providerItem.SafeHeight), MinCoin: req.MinCoin, Status: defaulted(req.Status, defaultGameStatus), SortOrder: int32((index + 1) * 10), @@ -626,6 +628,45 @@ func stringFromJSONValue(value any) string { } } +func int32FromJSONValue(value any) int32 { + switch typed := value.(type) { + case nil: + return 0 + case int: + if typed > 0 { + return int32(typed) + } + case int32: + if typed > 0 { + return typed + } + case int64: + if typed > 0 { + return int32(typed) + } + case float64: + if typed > 0 { + return int32(typed) + } + case json.Number: + parsed, err := strconv.ParseInt(typed.String(), 10, 32) + if err == nil && parsed > 0 { + return int32(parsed) + } + case string: + parsed, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 32) + if err == nil && parsed > 0 { + return int32(parsed) + } + default: + parsed, err := strconv.ParseInt(strings.TrimSpace(fmt.Sprint(typed)), 10, 32) + if err == nil && parsed > 0 { + return int32(parsed) + } + } + return 0 +} + func baishunMD5(value string) string { sum := md5.Sum([]byte(value)) return hex.EncodeToString(sum[:]) diff --git a/server/admin/internal/modules/gamemanagement/sync_games_test.go b/server/admin/internal/modules/gamemanagement/sync_games_test.go index c6b52176..a3a2a1b3 100644 --- a/server/admin/internal/modules/gamemanagement/sync_games_test.go +++ b/server/admin/internal/modules/gamemanagement/sync_games_test.go @@ -28,7 +28,7 @@ func TestFetchBaishunGameSyncPlanFetchesSignsAndFormatsCatalog(t *testing.T) { t.Fatalf("request body mismatch: %+v", body) } w.Header().Set("Content-Type", "application/json") - _, _ = w.Write([]byte(`{"code":0,"message":"ok","data":[{"game_id":1006,"name":"Gold of Test","preview_url":"https://cdn.test/1006.png","download_url":"https://h5.test/1006/index.html","game_orientation":2}]}`)) + _, _ = w.Write([]byte(`{"code":0,"message":"ok","data":[{"game_id":1006,"name":"Gold of Test","preview_url":"https://cdn.test/1006.png","download_url":"https://h5.test/1006/index.html","game_orientation":2,"safe_height":1024}]}`)) })) defer server.Close() @@ -58,6 +58,9 @@ func TestFetchBaishunGameSyncPlanFetchesSignsAndFormatsCatalog(t *testing.T) { if game.IconURL != "https://cdn.test/1006.png" || game.CoverURL != "https://cdn.test/1006.png" || game.Orientation != "landscape" || game.Status != "disabled" || game.LaunchMode != "full_screen" { t.Fatalf("catalog presentation mismatch: %+v", game) } + if game.SafeHeight != 1024 { + t.Fatalf("safeHeight = %d, want 1024", game.SafeHeight) + } var merged struct { GameURLs map[string]string `json:"game_urls"` } diff --git a/services/game-service/deploy/mysql/initdb/001_game_service.sql b/services/game-service/deploy/mysql/initdb/001_game_service.sql index 4bc41da9..934660af 100644 --- a/services/game-service/deploy/mysql/initdb/001_game_service.sql +++ b/services/game-service/deploy/mysql/initdb/001_game_service.sql @@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS game_catalog ( cover_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '封面 URL', launch_mode VARCHAR(32) NOT NULL DEFAULT 'h5_popup' COMMENT '启动模式', orientation VARCHAR(32) NOT NULL DEFAULT 'portrait' COMMENT '屏幕方向', + safe_height INT NOT NULL DEFAULT 0 COMMENT '游戏安全高', min_coin BIGINT NOT NULL DEFAULT 0 COMMENT '最小金币', status VARCHAR(32) NOT NULL COMMENT '业务状态', sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前', diff --git a/services/game-service/internal/domain/game/game.go b/services/game-service/internal/domain/game/game.go index fc9e3957..fb67d132 100644 --- a/services/game-service/internal/domain/game/game.go +++ b/services/game-service/internal/domain/game/game.go @@ -66,6 +66,7 @@ type CatalogItem struct { CoverURL string LaunchMode string Orientation string + SafeHeight int32 MinCoin int64 Status string SortOrder int32 @@ -85,6 +86,7 @@ type AppGame struct { Category string LaunchMode string Orientation string + SafeHeight int32 MinCoin int64 Enabled bool Maintenance bool diff --git a/services/game-service/internal/service/game/service.go b/services/game-service/internal/service/game/service.go index cf981eb3..e7bd221d 100644 --- a/services/game-service/internal/service/game/service.go +++ b/services/game-service/internal/service/game/service.go @@ -116,6 +116,7 @@ type LaunchResult struct { LaunchURL string ExpiresAtMS int64 Orientation string + SafeHeight int32 ServerTimeMS int64 } @@ -250,6 +251,7 @@ func (s *Service) LaunchGame(ctx context.Context, command LaunchCommand) (Launch LaunchURL: launchURL, ExpiresAtMS: expiresAt, Orientation: game.Orientation, + SafeHeight: game.SafeHeight, ServerTimeMS: now.UnixMilli(), }, nil } diff --git a/services/game-service/internal/storage/mysql/repository.go b/services/game-service/internal/storage/mysql/repository.go index a74d866d..cc8ce4bb 100644 --- a/services/game-service/internal/storage/mysql/repository.go +++ b/services/game-service/internal/storage/mysql/repository.go @@ -86,6 +86,7 @@ func (r *Repository) Migrate(ctx context.Context) error { cover_url VARCHAR(512) NOT NULL DEFAULT '', launch_mode VARCHAR(32) NOT NULL DEFAULT 'h5_popup', orientation VARCHAR(32) NOT NULL DEFAULT 'portrait', + safe_height INT NOT NULL DEFAULT 0, min_coin BIGINT NOT NULL DEFAULT 0, status VARCHAR(32) NOT NULL, sort_order INT NOT NULL DEFAULT 0, @@ -252,6 +253,9 @@ func (r *Repository) Migrate(ctx context.Context) error { if err := r.ensureColumn(ctx, "game_platforms", "adapter_type", "adapter_type VARCHAR(64) NOT NULL DEFAULT 'demo' AFTER api_base_url"); err != nil { return err } + if err := r.ensureColumn(ctx, "game_catalog", "safe_height", "safe_height INT NOT NULL DEFAULT 0 AFTER orientation"); err != nil { + return err + } if err := r.ensureColumn(ctx, "game_launch_sessions", "region_id", "region_id BIGINT NOT NULL DEFAULT 0 AFTER room_id"); err != nil { return err } @@ -355,7 +359,7 @@ func equalStringSlices(left []string, right []string) bool { func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQuery) ([]gamedomain.AppGame, error) { rows, err := r.db.QueryContext(ctx, `SELECT c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category, - c.launch_mode, c.orientation, c.min_coin, c.status, p.status, + c.launch_mode, c.orientation, c.safe_height, c.min_coin, c.status, p.status, MIN(CASE WHEN r.sort_order = 0 THEN c.sort_order ELSE r.sort_order END) AS display_sort FROM game_catalog c JOIN game_platforms p ON p.app_code = c.app_code AND p.platform_code = c.platform_code @@ -370,7 +374,7 @@ func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQ AND (r.language = '' OR r.language = ?) AND (r.platform = '' OR r.platform = ?) GROUP BY c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category, - c.launch_mode, c.orientation, c.min_coin, c.status, p.status, c.sort_order, p.sort_order + c.launch_mode, c.orientation, c.safe_height, c.min_coin, c.status, p.status, c.sort_order, p.sort_order ORDER BY display_sort ASC, p.sort_order ASC, c.sort_order ASC, c.game_id ASC`, appcode.Normalize(query.AppCode), query.Scene, query.RegionID, query.Language, query.ClientPlatform, ) @@ -382,7 +386,7 @@ func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQ for rows.Next() { var item gamedomain.AppGame var gameStatus, platformStatus string - if err := rows.Scan(&item.GameID, &item.PlatformCode, &item.Name, &item.IconURL, &item.CoverURL, &item.Category, &item.LaunchMode, &item.Orientation, &item.MinCoin, &gameStatus, &platformStatus, &item.SortOrder); err != nil { + if err := rows.Scan(&item.GameID, &item.PlatformCode, &item.Name, &item.IconURL, &item.CoverURL, &item.Category, &item.LaunchMode, &item.Orientation, &item.SafeHeight, &item.MinCoin, &gameStatus, &platformStatus, &item.SortOrder); err != nil { return nil, err } item.NameKey = "game." + item.GameID + ".name" @@ -397,7 +401,7 @@ func (r *Repository) GetLaunchableGame(ctx context.Context, appCode string, game // 启动游戏时一次性取出 catalog + platform 配置,避免拿到游戏后再查平台导致配置不一致。 row := r.db.QueryRowContext(ctx, `SELECT c.app_code, c.game_id, c.platform_code, c.provider_game_id, c.game_name, c.category, - c.icon_url, c.cover_url, c.launch_mode, c.orientation, c.min_coin, c.status, + c.icon_url, c.cover_url, c.launch_mode, c.orientation, c.safe_height, c.min_coin, c.status, c.sort_order, COALESCE(CAST(c.tags AS CHAR), '[]'), c.created_at_ms, c.updated_at_ms, p.platform_name, p.status, p.api_base_url, COALESCE(p.adapter_type, 'demo'), p.callback_secret_ciphertext, COALESCE(CAST(p.callback_ip_whitelist AS CHAR), '[]'), @@ -410,7 +414,7 @@ func (r *Repository) GetLaunchableGame(ctx context.Context, appCode string, game var item gamedomain.LaunchableGame var tagsJSON string var whitelistJSON string - if err := row.Scan(&item.AppCode, &item.GameID, &item.PlatformCode, &item.ProviderGameID, &item.GameName, &item.Category, &item.IconURL, &item.CoverURL, &item.LaunchMode, &item.Orientation, &item.MinCoin, &item.Status, &item.SortOrder, &tagsJSON, &item.CreatedAtMS, &item.UpdatedAtMS, &item.PlatformName, &item.PlatformStatus, &item.APIBaseURL, &item.AdapterType, &item.CallbackSecretCiphertext, &whitelistJSON, &item.AdapterConfigJSON); err != nil { + if err := row.Scan(&item.AppCode, &item.GameID, &item.PlatformCode, &item.ProviderGameID, &item.GameName, &item.Category, &item.IconURL, &item.CoverURL, &item.LaunchMode, &item.Orientation, &item.SafeHeight, &item.MinCoin, &item.Status, &item.SortOrder, &tagsJSON, &item.CreatedAtMS, &item.UpdatedAtMS, &item.PlatformName, &item.PlatformStatus, &item.APIBaseURL, &item.AdapterType, &item.CallbackSecretCiphertext, &whitelistJSON, &item.AdapterConfigJSON); err != nil { if errors.Is(err, sql.ErrNoRows) { return gamedomain.LaunchableGame{}, xerr.New(xerr.NotFound, "game not found") } @@ -1006,7 +1010,7 @@ func (r *Repository) UpsertPlatform(ctx context.Context, platform gamedomain.Pla func (r *Repository) ListCatalog(ctx context.Context, query gameservice.ListCatalogQuery) ([]gamedomain.CatalogItem, string, error) { sqlText := `SELECT app_code, game_id, platform_code, provider_game_id, game_name, category, icon_url, cover_url, - launch_mode, orientation, min_coin, status, sort_order, COALESCE(CAST(tags AS CHAR), '[]'), created_at_ms, updated_at_ms + launch_mode, orientation, safe_height, min_coin, status, sort_order, COALESCE(CAST(tags AS CHAR), '[]'), created_at_ms, updated_at_ms FROM game_catalog WHERE app_code = ?` args := []any{appcode.Normalize(query.AppCode)} if strings.TrimSpace(query.PlatformCode) != "" { @@ -1054,14 +1058,14 @@ func (r *Repository) UpsertCatalog(ctx context.Context, item gamedomain.CatalogI if _, err := tx.ExecContext(ctx, `INSERT INTO game_catalog ( app_code, game_id, platform_code, provider_game_id, game_name, category, icon_url, cover_url, - launch_mode, orientation, min_coin, status, sort_order, tags, created_at_ms, updated_at_ms - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + launch_mode, orientation, safe_height, min_coin, status, sort_order, tags, created_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE platform_code = VALUES(platform_code), provider_game_id = VALUES(provider_game_id), game_name = VALUES(game_name), category = VALUES(category), icon_url = VALUES(icon_url), cover_url = VALUES(cover_url), - launch_mode = VALUES(launch_mode), orientation = VALUES(orientation), min_coin = VALUES(min_coin), + launch_mode = VALUES(launch_mode), orientation = VALUES(orientation), safe_height = VALUES(safe_height), min_coin = VALUES(min_coin), status = VALUES(status), sort_order = VALUES(sort_order), tags = VALUES(tags), updated_at_ms = VALUES(updated_at_ms)`, item.AppCode, item.GameID, item.PlatformCode, item.ProviderGameID, item.GameName, item.Category, item.IconURL, item.CoverURL, - item.LaunchMode, item.Orientation, item.MinCoin, item.Status, item.SortOrder, string(tagsJSON), item.CreatedAtMS, item.UpdatedAtMS, + item.LaunchMode, item.Orientation, item.SafeHeight, item.MinCoin, item.Status, item.SortOrder, string(tagsJSON), item.CreatedAtMS, item.UpdatedAtMS, ); err != nil { return gamedomain.CatalogItem{}, err } @@ -1129,7 +1133,7 @@ func (r *Repository) DeleteCatalog(ctx context.Context, appCode string, gameID s func (r *Repository) getCatalog(ctx context.Context, appCode string, gameID string) (gamedomain.CatalogItem, error) { row := r.db.QueryRowContext(ctx, `SELECT app_code, game_id, platform_code, provider_game_id, game_name, category, icon_url, cover_url, - launch_mode, orientation, min_coin, status, sort_order, COALESCE(CAST(tags AS CHAR), '[]'), created_at_ms, updated_at_ms + launch_mode, orientation, safe_height, min_coin, status, sort_order, COALESCE(CAST(tags AS CHAR), '[]'), created_at_ms, updated_at_ms FROM game_catalog WHERE app_code = ? AND game_id = ?`, appcode.Normalize(appCode), strings.TrimSpace(gameID), ) @@ -1194,7 +1198,7 @@ func levelEventOutboxSelectSQL() string { func scanCatalog(scanner catalogScanner) (gamedomain.CatalogItem, error) { var item gamedomain.CatalogItem var tagsJSON string - if err := scanner.Scan(&item.AppCode, &item.GameID, &item.PlatformCode, &item.ProviderGameID, &item.GameName, &item.Category, &item.IconURL, &item.CoverURL, &item.LaunchMode, &item.Orientation, &item.MinCoin, &item.Status, &item.SortOrder, &tagsJSON, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil { + if err := scanner.Scan(&item.AppCode, &item.GameID, &item.PlatformCode, &item.ProviderGameID, &item.GameName, &item.Category, &item.IconURL, &item.CoverURL, &item.LaunchMode, &item.Orientation, &item.SafeHeight, &item.MinCoin, &item.Status, &item.SortOrder, &tagsJSON, &item.CreatedAtMS, &item.UpdatedAtMS); err != nil { return gamedomain.CatalogItem{}, err } _ = json.Unmarshal([]byte(tagsJSON), &item.Tags) diff --git a/services/game-service/internal/transport/grpc/server.go b/services/game-service/internal/transport/grpc/server.go index e5bac842..60fa90db 100644 --- a/services/game-service/internal/transport/grpc/server.go +++ b/services/game-service/internal/transport/grpc/server.go @@ -67,6 +67,7 @@ func (s *Server) LaunchGame(ctx context.Context, req *gamev1.LaunchGameRequest) LaunchUrl: result.LaunchURL, ExpiresAtMs: result.ExpiresAtMS, Orientation: result.Orientation, + SafeHeight: result.SafeHeight, ServerTimeMs: result.ServerTimeMS, }, nil } @@ -260,6 +261,7 @@ func appGameToProto(item gamedomain.AppGame) *gamev1.AppGame { Category: item.Category, LaunchMode: item.LaunchMode, Orientation: item.Orientation, + SafeHeight: item.SafeHeight, MinCoin: item.MinCoin, Enabled: item.Enabled, Maintenance: item.Maintenance, @@ -318,6 +320,7 @@ func catalogToProto(item gamedomain.CatalogItem) *gamev1.GameCatalogItem { CoverUrl: item.CoverURL, LaunchMode: item.LaunchMode, Orientation: item.Orientation, + SafeHeight: item.SafeHeight, MinCoin: item.MinCoin, Status: item.Status, SortOrder: item.SortOrder, @@ -342,6 +345,7 @@ func catalogFromProto(item *gamev1.GameCatalogItem) gamedomain.CatalogItem { CoverURL: item.GetCoverUrl(), LaunchMode: item.GetLaunchMode(), Orientation: item.GetOrientation(), + SafeHeight: item.GetSafeHeight(), MinCoin: item.GetMinCoin(), Status: item.GetStatus(), SortOrder: item.GetSortOrder(), @@ -383,6 +387,9 @@ func normalizeCatalog(item gamedomain.CatalogItem) (gamedomain.CatalogItem, erro if item.Orientation == "" { item.Orientation = "portrait" } + if item.SafeHeight < 0 { + item.SafeHeight = 0 + } if item.GameID == "" || item.PlatformCode == "" || item.ProviderGameID == "" || item.GameName == "" || item.Status == "" { return gamedomain.CatalogItem{}, xerr.New(xerr.InvalidArgument, "game is incomplete") } diff --git a/services/gateway-service/internal/transport/http/gameapi/game_view.go b/services/gateway-service/internal/transport/http/gameapi/game_view.go index 66f648b3..8af969a6 100644 --- a/services/gateway-service/internal/transport/http/gameapi/game_view.go +++ b/services/gateway-service/internal/transport/http/gameapi/game_view.go @@ -17,6 +17,8 @@ type gameItemData struct { Category string `json:"category"` LaunchMode string `json:"launch_mode"` Orientation string `json:"orientation"` + SafeHeight int32 `json:"safe_height"` + SafeHeightV2 int32 `json:"safeHeight"` MinCoin int64 `json:"min_coin"` Enabled bool `json:"enabled"` Maintenance bool `json:"maintenance"` @@ -28,6 +30,8 @@ type gameLaunchData struct { LaunchURL string `json:"launch_url"` ExpiresAtMS int64 `json:"expires_at_ms"` Orientation string `json:"orientation"` + SafeHeight int32 `json:"safe_height"` + SafeHeightV2 int32 `json:"safeHeight"` ServerTimeMS int64 `json:"server_time_ms"` } @@ -47,6 +51,8 @@ func gameListDataFromProto(resp *gamev1.ListGamesResponse) gameListData { Category: item.GetCategory(), LaunchMode: item.GetLaunchMode(), Orientation: item.GetOrientation(), + SafeHeight: item.GetSafeHeight(), + SafeHeightV2: item.GetSafeHeight(), MinCoin: item.GetMinCoin(), Enabled: item.GetEnabled(), Maintenance: item.GetMaintenance(), @@ -65,6 +71,8 @@ func gameLaunchDataFromProto(resp *gamev1.LaunchGameResponse) gameLaunchData { LaunchURL: resp.GetLaunchUrl(), ExpiresAtMS: resp.GetExpiresAtMs(), Orientation: resp.GetOrientation(), + SafeHeight: resp.GetSafeHeight(), + SafeHeightV2: resp.GetSafeHeight(), ServerTimeMS: resp.GetServerTimeMs(), } }