feat: persist game safe height

This commit is contained in:
zhx 2026-06-02 17:42:50 +08:00
parent 46e280c8a8
commit a9456239cc
12 changed files with 123 additions and 18 deletions

View File

@ -265,6 +265,7 @@ type GameCatalogItem struct {
Tags []string `protobuf:"bytes,14,rep,name=tags,proto3" json:"tags,omitempty"` 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"` 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -411,6 +412,13 @@ func (x *GameCatalogItem) GetUpdatedAtMs() int64 {
return 0 return 0
} }
func (x *GameCatalogItem) GetSafeHeight() int32 {
if x != nil {
return x.SafeHeight
}
return 0
}
type AppGame struct { type AppGame struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
GameId string `protobuf:"bytes,1,opt,name=game_id,json=gameId,proto3" json:"game_id,omitempty"` 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"` Enabled bool `protobuf:"varint,11,opt,name=enabled,proto3" json:"enabled,omitempty"`
Maintenance bool `protobuf:"varint,12,opt,name=maintenance,proto3" json:"maintenance,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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -551,6 +560,13 @@ func (x *AppGame) GetSortOrder() int32 {
return 0 return 0
} }
func (x *AppGame) GetSafeHeight() int32 {
if x != nil {
return x.SafeHeight
}
return 0
}
type ListGamesRequest struct { type ListGamesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` 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"` 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"` 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"` 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 unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@ -865,6 +882,13 @@ func (x *LaunchGameResponse) GetServerTimeMs() int64 {
return 0 return 0
} }
func (x *LaunchGameResponse) GetSafeHeight() int32 {
if x != nil {
return x.SafeHeight
}
return 0
}
type CallbackRequest struct { type CallbackRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` 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" + " \x01(\tR\x0ecallbackSecret\x12.\n" +
"\x13callback_secret_set\x18\v \x01(\bR\x11callbackSecretSet\x122\n" + "\x13callback_secret_set\x18\v \x01(\bR\x11callbackSecretSet\x122\n" +
"\x15callback_ip_whitelist\x18\f \x03(\tR\x13callbackIpWhitelist\x12.\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" + "\x0fGameCatalogItem\x12\x19\n" +
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x17\n" + "\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x17\n" +
"\agame_id\x18\x02 \x01(\tR\x06gameId\x12#\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" + "sort_order\x18\r \x01(\x05R\tsortOrder\x12\x12\n" +
"\x04tags\x18\x0e \x03(\tR\x04tags\x12\"\n" + "\x04tags\x18\x0e \x03(\tR\x04tags\x12\"\n" +
"\rcreated_at_ms\x18\x0f \x01(\x03R\vcreatedAtMs\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" + "\aAppGame\x12\x17\n" +
"\agame_id\x18\x01 \x01(\tR\x06gameId\x12#\n" + "\agame_id\x18\x01 \x01(\tR\x06gameId\x12#\n" +
"\rplatform_code\x18\x02 \x01(\tR\fplatformCode\x12\x19\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" + "\aenabled\x18\v \x01(\bR\aenabled\x12 \n" +
"\vmaintenance\x18\f \x01(\bR\vmaintenance\x12\x1d\n" + "\vmaintenance\x18\f \x01(\bR\vmaintenance\x12\x1d\n" +
"\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" + "\x10ListGamesRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.game.v1.RequestMetaR\x04meta\x12\x17\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" + "\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" + "\agame_id\x18\x04 \x01(\tR\x06gameId\x12\x14\n" +
"\x05scene\x18\x05 \x01(\tR\x05scene\x12\x17\n" + "\x05scene\x18\x05 \x01(\tR\x05scene\x12\x17\n" +
"\aroom_id\x18\x06 \x01(\tR\x06roomId\x12!\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" + "\x12LaunchGameResponse\x12\x1d\n" +
"\n" + "\n" +
"session_id\x18\x01 \x01(\tR\tsessionId\x12\x1d\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" + "launch_url\x18\x02 \x01(\tR\tlaunchUrl\x12\"\n" +
"\rexpires_at_ms\x18\x03 \x01(\x03R\vexpiresAtMs\x12 \n" + "\rexpires_at_ms\x18\x03 \x01(\x03R\vexpiresAtMs\x12 \n" +
"\vorientation\x18\x04 \x01(\tR\vorientation\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" + "\x0fCallbackRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.game.v1.RequestMetaR\x04meta\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" + "\rplatform_code\x18\x02 \x01(\tR\fplatformCode\x12\x1c\n" +

View File

@ -48,6 +48,7 @@ message GameCatalogItem {
repeated string tags = 14; repeated string tags = 14;
int64 created_at_ms = 15; int64 created_at_ms = 15;
int64 updated_at_ms = 16; int64 updated_at_ms = 16;
int32 safe_height = 17;
} }
message AppGame { message AppGame {
@ -64,6 +65,7 @@ message AppGame {
bool enabled = 11; bool enabled = 11;
bool maintenance = 12; bool maintenance = 12;
int32 sort_order = 13; int32 sort_order = 13;
int32 safe_height = 14;
} }
message ListGamesRequest { message ListGamesRequest {
@ -99,6 +101,7 @@ message LaunchGameResponse {
int64 expires_at_ms = 3; int64 expires_at_ms = 3;
string orientation = 4; string orientation = 4;
int64 server_time_ms = 5; int64 server_time_ms = 5;
int32 safe_height = 6;
} }
message CallbackRequest { message CallbackRequest {

View File

@ -34,6 +34,7 @@ type catalogDTO struct {
LaunchURL string `json:"launchUrl,omitempty"` LaunchURL string `json:"launchUrl,omitempty"`
LaunchMode string `json:"launchMode"` LaunchMode string `json:"launchMode"`
Orientation string `json:"orientation"` Orientation string `json:"orientation"`
SafeHeight int32 `json:"safeHeight"`
MinCoin int64 `json:"minCoin"` MinCoin int64 `json:"minCoin"`
Status string `json:"status"` Status string `json:"status"`
SortOrder int32 `json:"sortOrder"` SortOrder int32 `json:"sortOrder"`
@ -78,6 +79,7 @@ func catalogFromProto(item *gamev1.GameCatalogItem) catalogDTO {
CoverURL: item.GetCoverUrl(), CoverURL: item.GetCoverUrl(),
LaunchMode: item.GetLaunchMode(), LaunchMode: item.GetLaunchMode(),
Orientation: item.GetOrientation(), Orientation: item.GetOrientation(),
SafeHeight: item.GetSafeHeight(),
MinCoin: item.GetMinCoin(), MinCoin: item.GetMinCoin(),
Status: item.GetStatus(), Status: item.GetStatus(),
SortOrder: item.GetSortOrder(), SortOrder: item.GetSortOrder(),

View File

@ -36,6 +36,7 @@ type catalogRequest struct {
CoverURL string `json:"coverUrl"` CoverURL string `json:"coverUrl"`
LaunchMode string `json:"launchMode"` LaunchMode string `json:"launchMode"`
Orientation string `json:"orientation"` Orientation string `json:"orientation"`
SafeHeight int32 `json:"safeHeight"`
MinCoin int64 `json:"minCoin"` MinCoin int64 `json:"minCoin"`
Status string `json:"status"` Status string `json:"status"`
SortOrder int32 `json:"sortOrder"` SortOrder int32 `json:"sortOrder"`
@ -77,6 +78,7 @@ func (r catalogRequest) toProto(gameID string) *gamev1.GameCatalogItem {
CoverUrl: strings.TrimSpace(r.CoverURL), CoverUrl: strings.TrimSpace(r.CoverURL),
LaunchMode: strings.TrimSpace(r.LaunchMode), LaunchMode: strings.TrimSpace(r.LaunchMode),
Orientation: strings.TrimSpace(r.Orientation), Orientation: strings.TrimSpace(r.Orientation),
SafeHeight: r.SafeHeight,
MinCoin: r.MinCoin, MinCoin: r.MinCoin,
Status: strings.TrimSpace(r.Status), Status: strings.TrimSpace(r.Status),
SortOrder: r.SortOrder, SortOrder: r.SortOrder,

View File

@ -478,6 +478,7 @@ type baishunGameListItem struct {
CoverURL string `json:"cover_url"` CoverURL string `json:"cover_url"`
DownloadURL string `json:"download_url"` DownloadURL string `json:"download_url"`
GameOrientation any `json:"game_orientation"` GameOrientation any `json:"game_orientation"`
SafeHeight any `json:"safe_height"`
} }
func baishunCatalogItems(platformCode string, providerItems []baishunGameListItem, req syncGamesRequest) ([]catalogRequest, map[string]string) { func baishunCatalogItems(platformCode string, providerItems []baishunGameListItem, req syncGamesRequest) ([]catalogRequest, map[string]string) {
@ -502,6 +503,7 @@ func baishunCatalogItems(platformCode string, providerItems []baishunGameListIte
CoverURL: coverURL, CoverURL: coverURL,
LaunchMode: defaulted(req.LaunchMode, defaultLaunchMode), LaunchMode: defaulted(req.LaunchMode, defaultLaunchMode),
Orientation: baishunOrientation(providerItem.GameOrientation), Orientation: baishunOrientation(providerItem.GameOrientation),
SafeHeight: int32FromJSONValue(providerItem.SafeHeight),
MinCoin: req.MinCoin, MinCoin: req.MinCoin,
Status: defaulted(req.Status, defaultGameStatus), Status: defaulted(req.Status, defaultGameStatus),
SortOrder: int32((index + 1) * 10), 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 { func baishunMD5(value string) string {
sum := md5.Sum([]byte(value)) sum := md5.Sum([]byte(value))
return hex.EncodeToString(sum[:]) return hex.EncodeToString(sum[:])

View File

@ -28,7 +28,7 @@ func TestFetchBaishunGameSyncPlanFetchesSignsAndFormatsCatalog(t *testing.T) {
t.Fatalf("request body mismatch: %+v", body) t.Fatalf("request body mismatch: %+v", body)
} }
w.Header().Set("Content-Type", "application/json") 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() 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" { 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) t.Fatalf("catalog presentation mismatch: %+v", game)
} }
if game.SafeHeight != 1024 {
t.Fatalf("safeHeight = %d, want 1024", game.SafeHeight)
}
var merged struct { var merged struct {
GameURLs map[string]string `json:"game_urls"` GameURLs map[string]string `json:"game_urls"`
} }

View File

@ -33,6 +33,7 @@ CREATE TABLE IF NOT EXISTS game_catalog (
cover_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '封面 URL', cover_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '封面 URL',
launch_mode VARCHAR(32) NOT NULL DEFAULT 'h5_popup' COMMENT '启动模式', launch_mode VARCHAR(32) NOT NULL DEFAULT 'h5_popup' COMMENT '启动模式',
orientation VARCHAR(32) NOT NULL DEFAULT 'portrait' 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 '最小金币', min_coin BIGINT NOT NULL DEFAULT 0 COMMENT '最小金币',
status VARCHAR(32) NOT NULL COMMENT '业务状态', status VARCHAR(32) NOT NULL COMMENT '业务状态',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前', sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',

View File

@ -66,6 +66,7 @@ type CatalogItem struct {
CoverURL string CoverURL string
LaunchMode string LaunchMode string
Orientation string Orientation string
SafeHeight int32
MinCoin int64 MinCoin int64
Status string Status string
SortOrder int32 SortOrder int32
@ -85,6 +86,7 @@ type AppGame struct {
Category string Category string
LaunchMode string LaunchMode string
Orientation string Orientation string
SafeHeight int32
MinCoin int64 MinCoin int64
Enabled bool Enabled bool
Maintenance bool Maintenance bool

View File

@ -116,6 +116,7 @@ type LaunchResult struct {
LaunchURL string LaunchURL string
ExpiresAtMS int64 ExpiresAtMS int64
Orientation string Orientation string
SafeHeight int32
ServerTimeMS int64 ServerTimeMS int64
} }
@ -250,6 +251,7 @@ func (s *Service) LaunchGame(ctx context.Context, command LaunchCommand) (Launch
LaunchURL: launchURL, LaunchURL: launchURL,
ExpiresAtMS: expiresAt, ExpiresAtMS: expiresAt,
Orientation: game.Orientation, Orientation: game.Orientation,
SafeHeight: game.SafeHeight,
ServerTimeMS: now.UnixMilli(), ServerTimeMS: now.UnixMilli(),
}, nil }, nil
} }

View File

@ -86,6 +86,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
cover_url VARCHAR(512) NOT NULL DEFAULT '', cover_url VARCHAR(512) NOT NULL DEFAULT '',
launch_mode VARCHAR(32) NOT NULL DEFAULT 'h5_popup', launch_mode VARCHAR(32) NOT NULL DEFAULT 'h5_popup',
orientation VARCHAR(32) NOT NULL DEFAULT 'portrait', orientation VARCHAR(32) NOT NULL DEFAULT 'portrait',
safe_height INT NOT NULL DEFAULT 0,
min_coin BIGINT NOT NULL DEFAULT 0, min_coin BIGINT NOT NULL DEFAULT 0,
status VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL,
sort_order INT NOT NULL DEFAULT 0, 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 { 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 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 { if err := r.ensureColumn(ctx, "game_launch_sessions", "region_id", "region_id BIGINT NOT NULL DEFAULT 0 AFTER room_id"); err != nil {
return err 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) { func (r *Repository) ListGames(ctx context.Context, query gameservice.ListGamesQuery) ([]gamedomain.AppGame, error) {
rows, err := r.db.QueryContext(ctx, rows, err := r.db.QueryContext(ctx,
`SELECT c.game_id, c.platform_code, c.game_name, c.icon_url, c.cover_url, c.category, `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 MIN(CASE WHEN r.sort_order = 0 THEN c.sort_order ELSE r.sort_order END) AS display_sort
FROM game_catalog c FROM game_catalog c
JOIN game_platforms p ON p.app_code = c.app_code AND p.platform_code = c.platform_code 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.language = '' OR r.language = ?)
AND (r.platform = '' OR r.platform = ?) 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, 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`, 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, 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() { for rows.Next() {
var item gamedomain.AppGame var item gamedomain.AppGame
var gameStatus, platformStatus string 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 return nil, err
} }
item.NameKey = "game." + item.GameID + ".name" item.NameKey = "game." + item.GameID + ".name"
@ -397,7 +401,7 @@ func (r *Repository) GetLaunchableGame(ctx context.Context, appCode string, game
// 启动游戏时一次性取出 catalog + platform 配置,避免拿到游戏后再查平台导致配置不一致。 // 启动游戏时一次性取出 catalog + platform 配置,避免拿到游戏后再查平台导致配置不一致。
row := r.db.QueryRowContext(ctx, row := r.db.QueryRowContext(ctx,
`SELECT c.app_code, c.game_id, c.platform_code, c.provider_game_id, c.game_name, c.category, `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, 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.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), '[]'), 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 item gamedomain.LaunchableGame
var tagsJSON string var tagsJSON string
var whitelistJSON 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) { if errors.Is(err, sql.ErrNoRows) {
return gamedomain.LaunchableGame{}, xerr.New(xerr.NotFound, "game not found") 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) { 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, 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 = ?` FROM game_catalog WHERE app_code = ?`
args := []any{appcode.Normalize(query.AppCode)} args := []any{appcode.Normalize(query.AppCode)}
if strings.TrimSpace(query.PlatformCode) != "" { if strings.TrimSpace(query.PlatformCode) != "" {
@ -1054,14 +1058,14 @@ func (r *Repository) UpsertCatalog(ctx context.Context, item gamedomain.CatalogI
if _, err := tx.ExecContext(ctx, if _, err := tx.ExecContext(ctx,
`INSERT INTO game_catalog ( `INSERT INTO game_catalog (
app_code, game_id, platform_code, provider_game_id, game_name, category, icon_url, cover_url, 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 launch_mode, orientation, safe_height, min_coin, status, sort_order, tags, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE platform_code = VALUES(platform_code), provider_game_id = VALUES(provider_game_id), 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), 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)`, 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.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 { ); err != nil {
return gamedomain.CatalogItem{}, err 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) { func (r *Repository) getCatalog(ctx context.Context, appCode string, gameID string) (gamedomain.CatalogItem, error) {
row := r.db.QueryRowContext(ctx, row := r.db.QueryRowContext(ctx,
`SELECT app_code, game_id, platform_code, provider_game_id, game_name, category, icon_url, cover_url, `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 = ?`, FROM game_catalog WHERE app_code = ? AND game_id = ?`,
appcode.Normalize(appCode), strings.TrimSpace(gameID), appcode.Normalize(appCode), strings.TrimSpace(gameID),
) )
@ -1194,7 +1198,7 @@ func levelEventOutboxSelectSQL() string {
func scanCatalog(scanner catalogScanner) (gamedomain.CatalogItem, error) { func scanCatalog(scanner catalogScanner) (gamedomain.CatalogItem, error) {
var item gamedomain.CatalogItem var item gamedomain.CatalogItem
var tagsJSON string 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 return gamedomain.CatalogItem{}, err
} }
_ = json.Unmarshal([]byte(tagsJSON), &item.Tags) _ = json.Unmarshal([]byte(tagsJSON), &item.Tags)

View File

@ -67,6 +67,7 @@ func (s *Server) LaunchGame(ctx context.Context, req *gamev1.LaunchGameRequest)
LaunchUrl: result.LaunchURL, LaunchUrl: result.LaunchURL,
ExpiresAtMs: result.ExpiresAtMS, ExpiresAtMs: result.ExpiresAtMS,
Orientation: result.Orientation, Orientation: result.Orientation,
SafeHeight: result.SafeHeight,
ServerTimeMs: result.ServerTimeMS, ServerTimeMs: result.ServerTimeMS,
}, nil }, nil
} }
@ -260,6 +261,7 @@ func appGameToProto(item gamedomain.AppGame) *gamev1.AppGame {
Category: item.Category, Category: item.Category,
LaunchMode: item.LaunchMode, LaunchMode: item.LaunchMode,
Orientation: item.Orientation, Orientation: item.Orientation,
SafeHeight: item.SafeHeight,
MinCoin: item.MinCoin, MinCoin: item.MinCoin,
Enabled: item.Enabled, Enabled: item.Enabled,
Maintenance: item.Maintenance, Maintenance: item.Maintenance,
@ -318,6 +320,7 @@ func catalogToProto(item gamedomain.CatalogItem) *gamev1.GameCatalogItem {
CoverUrl: item.CoverURL, CoverUrl: item.CoverURL,
LaunchMode: item.LaunchMode, LaunchMode: item.LaunchMode,
Orientation: item.Orientation, Orientation: item.Orientation,
SafeHeight: item.SafeHeight,
MinCoin: item.MinCoin, MinCoin: item.MinCoin,
Status: item.Status, Status: item.Status,
SortOrder: item.SortOrder, SortOrder: item.SortOrder,
@ -342,6 +345,7 @@ func catalogFromProto(item *gamev1.GameCatalogItem) gamedomain.CatalogItem {
CoverURL: item.GetCoverUrl(), CoverURL: item.GetCoverUrl(),
LaunchMode: item.GetLaunchMode(), LaunchMode: item.GetLaunchMode(),
Orientation: item.GetOrientation(), Orientation: item.GetOrientation(),
SafeHeight: item.GetSafeHeight(),
MinCoin: item.GetMinCoin(), MinCoin: item.GetMinCoin(),
Status: item.GetStatus(), Status: item.GetStatus(),
SortOrder: item.GetSortOrder(), SortOrder: item.GetSortOrder(),
@ -383,6 +387,9 @@ func normalizeCatalog(item gamedomain.CatalogItem) (gamedomain.CatalogItem, erro
if item.Orientation == "" { if item.Orientation == "" {
item.Orientation = "portrait" item.Orientation = "portrait"
} }
if item.SafeHeight < 0 {
item.SafeHeight = 0
}
if item.GameID == "" || item.PlatformCode == "" || item.ProviderGameID == "" || item.GameName == "" || item.Status == "" { if item.GameID == "" || item.PlatformCode == "" || item.ProviderGameID == "" || item.GameName == "" || item.Status == "" {
return gamedomain.CatalogItem{}, xerr.New(xerr.InvalidArgument, "game is incomplete") return gamedomain.CatalogItem{}, xerr.New(xerr.InvalidArgument, "game is incomplete")
} }

View File

@ -17,6 +17,8 @@ type gameItemData struct {
Category string `json:"category"` Category string `json:"category"`
LaunchMode string `json:"launch_mode"` LaunchMode string `json:"launch_mode"`
Orientation string `json:"orientation"` Orientation string `json:"orientation"`
SafeHeight int32 `json:"safe_height"`
SafeHeightV2 int32 `json:"safeHeight"`
MinCoin int64 `json:"min_coin"` MinCoin int64 `json:"min_coin"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Maintenance bool `json:"maintenance"` Maintenance bool `json:"maintenance"`
@ -28,6 +30,8 @@ type gameLaunchData struct {
LaunchURL string `json:"launch_url"` LaunchURL string `json:"launch_url"`
ExpiresAtMS int64 `json:"expires_at_ms"` ExpiresAtMS int64 `json:"expires_at_ms"`
Orientation string `json:"orientation"` Orientation string `json:"orientation"`
SafeHeight int32 `json:"safe_height"`
SafeHeightV2 int32 `json:"safeHeight"`
ServerTimeMS int64 `json:"server_time_ms"` ServerTimeMS int64 `json:"server_time_ms"`
} }
@ -47,6 +51,8 @@ func gameListDataFromProto(resp *gamev1.ListGamesResponse) gameListData {
Category: item.GetCategory(), Category: item.GetCategory(),
LaunchMode: item.GetLaunchMode(), LaunchMode: item.GetLaunchMode(),
Orientation: item.GetOrientation(), Orientation: item.GetOrientation(),
SafeHeight: item.GetSafeHeight(),
SafeHeightV2: item.GetSafeHeight(),
MinCoin: item.GetMinCoin(), MinCoin: item.GetMinCoin(),
Enabled: item.GetEnabled(), Enabled: item.GetEnabled(),
Maintenance: item.GetMaintenance(), Maintenance: item.GetMaintenance(),
@ -65,6 +71,8 @@ func gameLaunchDataFromProto(resp *gamev1.LaunchGameResponse) gameLaunchData {
LaunchURL: resp.GetLaunchUrl(), LaunchURL: resp.GetLaunchUrl(),
ExpiresAtMS: resp.GetExpiresAtMs(), ExpiresAtMS: resp.GetExpiresAtMs(),
Orientation: resp.GetOrientation(), Orientation: resp.GetOrientation(),
SafeHeight: resp.GetSafeHeight(),
SafeHeightV2: resp.GetSafeHeight(),
ServerTimeMS: resp.GetServerTimeMs(), ServerTimeMS: resp.GetServerTimeMs(),
} }
} }