From 9a9cccd194e112217c99b8f29db73edd3b15df3e Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 16 Jun 2026 14:11:07 +0800 Subject: [PATCH] fix: split room rps gift media urls --- api/proto/game/v1/game.pb.go | 33 ++++++++----- api/proto/game/v1/game.proto | 1 + .../internal/modules/gamemanagement/dto.go | 30 ++++++------ .../modules/gamemanagement/dto_test.go | 26 ++++++++++ .../internal/service/roomrps/service.go | 49 ++++++++++++++----- .../internal/service/roomrps/service_test.go | 46 ++++++++++++++--- .../transport/http/gameapi/game_view.go | 30 ++++++------ .../transport/http/gameapi/game_view_test.go | 37 ++++++++++++++ 8 files changed, 194 insertions(+), 58 deletions(-) create mode 100644 services/gateway-service/internal/transport/http/gameapi/game_view_test.go diff --git a/api/proto/game/v1/game.pb.go b/api/proto/game/v1/game.pb.go index 9f5a1b13..4674d170 100644 --- a/api/proto/game/v1/game.pb.go +++ b/api/proto/game/v1/game.pb.go @@ -2038,16 +2038,17 @@ func (x *DiceConfigResponse) GetServerTimeMs() int64 { } type RoomRPSStakeGift struct { - state protoimpl.MessageState `protogen:"open.v1"` - GiftId int64 `protobuf:"varint,1,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"` - GiftName string `protobuf:"bytes,2,opt,name=gift_name,json=giftName,proto3" json:"gift_name,omitempty"` - GiftIconUrl string `protobuf:"bytes,3,opt,name=gift_icon_url,json=giftIconUrl,proto3" json:"gift_icon_url,omitempty"` - GiftPriceCoin int64 `protobuf:"varint,4,opt,name=gift_price_coin,json=giftPriceCoin,proto3" json:"gift_price_coin,omitempty"` - Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` - SortOrder int32 `protobuf:"varint,6,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` - GiftIdText string `protobuf:"bytes,7,opt,name=gift_id_text,json=giftIdText,proto3" json:"gift_id_text,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + GiftId int64 `protobuf:"varint,1,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"` + GiftName string `protobuf:"bytes,2,opt,name=gift_name,json=giftName,proto3" json:"gift_name,omitempty"` + GiftIconUrl string `protobuf:"bytes,3,opt,name=gift_icon_url,json=giftIconUrl,proto3" json:"gift_icon_url,omitempty"` + GiftPriceCoin int64 `protobuf:"varint,4,opt,name=gift_price_coin,json=giftPriceCoin,proto3" json:"gift_price_coin,omitempty"` + Enabled bool `protobuf:"varint,5,opt,name=enabled,proto3" json:"enabled,omitempty"` + SortOrder int32 `protobuf:"varint,6,opt,name=sort_order,json=sortOrder,proto3" json:"sort_order,omitempty"` + GiftIdText string `protobuf:"bytes,7,opt,name=gift_id_text,json=giftIdText,proto3" json:"gift_id_text,omitempty"` + GiftAnimationUrl string `protobuf:"bytes,8,opt,name=gift_animation_url,json=giftAnimationUrl,proto3" json:"gift_animation_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RoomRPSStakeGift) Reset() { @@ -2129,6 +2130,13 @@ func (x *RoomRPSStakeGift) GetGiftIdText() string { return "" } +func (x *RoomRPSStakeGift) GetGiftAnimationUrl() string { + if x != nil { + return x.GiftAnimationUrl + } + return "" +} + type RoomRPSConfig struct { state protoimpl.MessageState `protogen:"open.v1"` AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` @@ -6530,7 +6538,7 @@ const file_proto_game_v1_game_proto_rawDesc = "" + "\agame_id\x18\x02 \x01(\tR\x06gameId\"m\n" + "\x12DiceConfigResponse\x121\n" + "\x06config\x18\x01 \x01(\v2\x19.hyapp.game.v1.DiceConfigR\x06config\x12$\n" + - "\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xef\x01\n" + + "\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\x9d\x02\n" + "\x10RoomRPSStakeGift\x12\x17\n" + "\agift_id\x18\x01 \x01(\x03R\x06giftId\x12\x1b\n" + "\tgift_name\x18\x02 \x01(\tR\bgiftName\x12\"\n" + @@ -6540,7 +6548,8 @@ const file_proto_game_v1_game_proto_rawDesc = "" + "\n" + "sort_order\x18\x06 \x01(\x05R\tsortOrder\x12 \n" + "\fgift_id_text\x18\a \x01(\tR\n" + - "giftIdText\"\xc7\x02\n" + + "giftIdText\x12,\n" + + "\x12gift_animation_url\x18\b \x01(\tR\x10giftAnimationUrl\"\xc7\x02\n" + "\rRoomRPSConfig\x12\x19\n" + "\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x17\n" + "\agame_id\x18\x02 \x01(\tR\x06gameId\x12\x16\n" + diff --git a/api/proto/game/v1/game.proto b/api/proto/game/v1/game.proto index a83797a0..bf39c9d4 100644 --- a/api/proto/game/v1/game.proto +++ b/api/proto/game/v1/game.proto @@ -238,6 +238,7 @@ message RoomRPSStakeGift { bool enabled = 5; int32 sort_order = 6; string gift_id_text = 7; + string gift_animation_url = 8; } message RoomRPSConfig { diff --git a/server/admin/internal/modules/gamemanagement/dto.go b/server/admin/internal/modules/gamemanagement/dto.go index e2cfd444..372c38d0 100644 --- a/server/admin/internal/modules/gamemanagement/dto.go +++ b/server/admin/internal/modules/gamemanagement/dto.go @@ -147,13 +147,14 @@ type roomRPSConfigDTO struct { } type roomRPSStakeGiftDTO struct { - GiftID string `json:"giftId"` - GiftIDNumber int64 `json:"giftIdNumber"` - GiftName string `json:"giftName"` - GiftIconURL string `json:"giftIconUrl"` - GiftPriceCoin int64 `json:"giftPriceCoin"` - Enabled bool `json:"enabled"` - SortOrder int32 `json:"sortOrder"` + GiftID string `json:"giftId"` + GiftIDNumber int64 `json:"giftIdNumber"` + GiftName string `json:"giftName"` + GiftIconURL string `json:"giftIconUrl"` + GiftAnimationURL string `json:"giftAnimationUrl"` + GiftPriceCoin int64 `json:"giftPriceCoin"` + Enabled bool `json:"enabled"` + SortOrder int32 `json:"sortOrder"` } type roomRPSChallengeDTO struct { @@ -353,13 +354,14 @@ func roomRPSConfigFromProto(item *gamev1.RoomRPSConfig) roomRPSConfigDTO { for _, gift := range item.GetStakeGifts() { // 礼物详情由 game-service 结合礼物配置补齐;admin-server 只保持展示字段和 ID 双写,不重新查礼物表。 gifts = append(gifts, roomRPSStakeGiftDTO{ - GiftID: roomRPSStakeGiftIDFromProto(gift), - GiftIDNumber: gift.GetGiftId(), - GiftName: gift.GetGiftName(), - GiftIconURL: gift.GetGiftIconUrl(), - GiftPriceCoin: gift.GetGiftPriceCoin(), - Enabled: gift.GetEnabled(), - SortOrder: gift.GetSortOrder(), + GiftID: roomRPSStakeGiftIDFromProto(gift), + GiftIDNumber: gift.GetGiftId(), + GiftName: gift.GetGiftName(), + GiftIconURL: gift.GetGiftIconUrl(), + GiftAnimationURL: gift.GetGiftAnimationUrl(), + GiftPriceCoin: gift.GetGiftPriceCoin(), + Enabled: gift.GetEnabled(), + SortOrder: gift.GetSortOrder(), }) } // DTO 只做 proto 到后台 JSON 的字段命名转换;配置默认值和合法性已经在 game-service/admin request 层处理。 diff --git a/server/admin/internal/modules/gamemanagement/dto_test.go b/server/admin/internal/modules/gamemanagement/dto_test.go index e069ae19..8965e522 100644 --- a/server/admin/internal/modules/gamemanagement/dto_test.go +++ b/server/admin/internal/modules/gamemanagement/dto_test.go @@ -113,6 +113,32 @@ func TestRoomRPSConfigRequestAcceptsStringGiftIDs(t *testing.T) { } } +func TestRoomRPSConfigFromProtoIncludesGiftAnimationURL(t *testing.T) { + got := roomRPSConfigFromProto(&gamev1.RoomRPSConfig{ + AppCode: "lalu", + GameId: "room_rps", + Status: "active", + StakeGifts: []*gamev1.RoomRPSStakeGift{ + { + GiftIdText: "87", + GiftName: "彩蛋兔", + GiftIconUrl: "https://cdn.example.test/gifts/egg-rabbit.png", + GiftAnimationUrl: "https://cdn.example.test/gifts/egg-rabbit.mp4", + GiftPriceCoin: 40000, + Enabled: true, + SortOrder: 1, + }, + }, + }) + + if len(got.StakeGifts) != 1 { + t.Fatalf("expected one stake gift, got %+v", got.StakeGifts) + } + if got.StakeGifts[0].GiftIconURL != "https://cdn.example.test/gifts/egg-rabbit.png" || got.StakeGifts[0].GiftAnimationURL != "https://cdn.example.test/gifts/egg-rabbit.mp4" { + t.Fatalf("room rps admin gift media fields mismatch: %+v", got.StakeGifts[0]) + } +} + func TestRoomRPSChallengeFromProtoKeepsLargeIDsAsStrings(t *testing.T) { got := roomRPSChallengeFromProto(&gamev1.RoomRPSChallenge{ ChallengeId: "rps-1", diff --git a/services/game-service/internal/service/roomrps/service.go b/services/game-service/internal/service/roomrps/service.go index 29feb04b..d7b83d65 100644 --- a/services/game-service/internal/service/roomrps/service.go +++ b/services/game-service/internal/service/roomrps/service.go @@ -508,13 +508,14 @@ func (s *Service) normalizeConfig(ctx context.Context, app string, config *gamev seenSort[sortOrder] = struct{}{} normalized := &gamev1.RoomRPSStakeGift{ - GiftId: legacyRoomRPSGiftID(giftID), - GiftIdText: giftID, - GiftName: strings.TrimSpace(gift.GetGiftName()), - GiftIconUrl: strings.TrimSpace(gift.GetGiftIconUrl()), - GiftPriceCoin: gift.GetGiftPriceCoin(), - Enabled: gift.GetEnabled(), - SortOrder: sortOrder, + GiftId: legacyRoomRPSGiftID(giftID), + GiftIdText: giftID, + GiftName: strings.TrimSpace(gift.GetGiftName()), + GiftIconUrl: strings.TrimSpace(gift.GetGiftIconUrl()), + GiftAnimationUrl: strings.TrimSpace(gift.GetGiftAnimationUrl()), + GiftPriceCoin: gift.GetGiftPriceCoin(), + Enabled: gift.GetEnabled(), + SortOrder: sortOrder, } gifts = append(gifts, normalized) } @@ -571,7 +572,7 @@ func (s *Service) enrichStakeGifts(ctx context.Context, app string, gifts []*gam if giftID == "" { continue } - if gift.GetGiftName() != "" && gift.GetGiftIconUrl() != "" && gift.GetGiftPriceCoin() > 0 { + if gift.GetGiftName() != "" && gift.GetGiftIconUrl() != "" && gift.GetGiftAnimationUrl() != "" && gift.GetGiftPriceCoin() > 0 && !roomRPSURLLooksAnimated(gift.GetGiftIconUrl()) { continue } if _, ok := seen[giftID]; ok { @@ -604,13 +605,20 @@ func (s *Service) enrichStakeGifts(ctx context.Context, app string, gifts []*gam changed = true } } - if gift.GiftIconUrl == "" { + if gift.GiftIconUrl == "" || roomRPSURLLooksAnimated(gift.GiftIconUrl) { if iconURL := roomRPSGiftIconURL(item); iconURL != "" { - // Flutter 的 PK 面板只消费 gift_icon_url;这里把礼物资源主图折叠成该字段,保持接口契约稳定。 + // gift_icon_url 只给静态封面;历史配置曾把 animation_url 写进这里,读配置时要用 wallet 资源重新纠正。 gift.GiftIconUrl = iconURL changed = true } } + if gift.GiftAnimationUrl == "" { + if animationURL := roomRPSGiftAnimationURL(item); animationURL != "" { + // 动效单独返回给客户端按需播放,不能再混入 icon 字段,否则列表和选择面板会把 mp4 当图片加载。 + gift.GiftAnimationUrl = animationURL + changed = true + } + } if gift.GiftPriceCoin <= 0 && item.GetCoinPrice() > 0 { // 创建挑战会把 stake_coin 固化到对局事实,缺币价时必须使用 wallet-service 当前生效礼物价格补齐。 gift.GiftPriceCoin = item.GetCoinPrice() @@ -680,7 +688,6 @@ func roomRPSGiftIconURL(gift *walletv1.GiftConfig) string { candidates := []string{ gift.GetResource().GetAssetUrl(), gift.GetResource().GetPreviewUrl(), - gift.GetResource().GetAnimationUrl(), } for _, candidate := range candidates { if value := strings.TrimSpace(candidate); value != "" { @@ -690,6 +697,26 @@ func roomRPSGiftIconURL(gift *walletv1.GiftConfig) string { return "" } +func roomRPSGiftAnimationURL(gift *walletv1.GiftConfig) string { + if gift == nil || gift.GetResource() == nil { + return "" + } + return strings.TrimSpace(gift.GetResource().GetAnimationUrl()) +} + +func roomRPSURLLooksAnimated(value string) bool { + value = strings.ToLower(strings.TrimSpace(value)) + if value == "" { + return false + } + for _, suffix := range []string{".mp4", ".svga", ".pag", ".webm", ".mov"} { + if strings.Contains(value, suffix) { + return true + } + } + return false +} + func (s *Service) publishChallengeEvent(ctx context.Context, eventType string, challenge *gamev1.RoomRPSChallenge) error { if s.pub == nil || challenge == nil { return nil diff --git a/services/game-service/internal/service/roomrps/service_test.go b/services/game-service/internal/service/roomrps/service_test.go index 59359755..6e48c67e 100644 --- a/services/game-service/internal/service/roomrps/service_test.go +++ b/services/game-service/internal/service/roomrps/service_test.go @@ -11,8 +11,8 @@ import ( func TestUpdateConfigEnrichesStakeGiftsFromWalletCatalog(t *testing.T) { catalog := newFakeGiftCatalog(map[string]*walletv1.GiftConfig{ - "2001": roomRPSWalletGift("2001", "Rose", "https://cdn.example.test/rose.png", "", "", 100), - "2002": roomRPSWalletGift("2002", "Bell", "", "https://cdn.example.test/bell-preview.png", "", 300), + "2001": roomRPSWalletGift("2001", "Rose", "https://cdn.example.test/rose.png", "", "https://cdn.example.test/rose.mp4", 100), + "2002": roomRPSWalletGift("2002", "Bell", "", "https://cdn.example.test/bell-preview.png", "https://cdn.example.test/bell.svga", 300), "2003": roomRPSWalletGift("2003", "Crown", "", "", "https://cdn.example.test/crown.svga", 500), "2004": roomRPSWalletGift("2004", "Cup", "https://cdn.example.test/cup.png", "", "", 1000), }) @@ -34,14 +34,14 @@ func TestUpdateConfigEnrichesStakeGiftsFromWalletCatalog(t *testing.T) { t.Fatalf("UpdateConfig() error = %v", err) } - if got := config.GetStakeGifts()[0]; got.GetGiftName() != "Rose" || got.GetGiftIconUrl() != "https://cdn.example.test/rose.png" || got.GetGiftPriceCoin() != 100 { + if got := config.GetStakeGifts()[0]; got.GetGiftName() != "Rose" || got.GetGiftIconUrl() != "https://cdn.example.test/rose.png" || got.GetGiftAnimationUrl() != "https://cdn.example.test/rose.mp4" || got.GetGiftPriceCoin() != 100 { t.Fatalf("first gift was not enriched from wallet catalog: %+v", got) } - if got := config.GetStakeGifts()[1]; got.GetGiftIconUrl() != "https://cdn.example.test/bell-preview.png" { - t.Fatalf("second gift should use preview_url when asset_url is empty: %+v", got) + if got := config.GetStakeGifts()[1]; got.GetGiftIconUrl() != "https://cdn.example.test/bell-preview.png" || got.GetGiftAnimationUrl() != "https://cdn.example.test/bell.svga" { + t.Fatalf("second gift should use preview_url as icon and animation_url as animation: %+v", got) } - if got := config.GetStakeGifts()[2]; got.GetGiftIconUrl() != "https://cdn.example.test/crown.svga" { - t.Fatalf("third gift should use animation_url as last display fallback: %+v", got) + if got := config.GetStakeGifts()[2]; got.GetGiftIconUrl() != "" || got.GetGiftAnimationUrl() != "https://cdn.example.test/crown.svga" { + t.Fatalf("third gift should not use animation_url as display icon: %+v", got) } if len(catalog.requests) != 4 { t.Fatalf("expected one exact catalog lookup per missing gift, got %d", len(catalog.requests)) @@ -89,6 +89,38 @@ func TestGetConfigBackfillsLegacyEmptyGiftSnapshot(t *testing.T) { } } +func TestGetConfigMovesLegacyAnimatedIconIntoAnimationURL(t *testing.T) { + catalog := newFakeGiftCatalog(map[string]*walletv1.GiftConfig{ + "Gifi-3": roomRPSWalletGift("Gifi-3", "Rocket", "https://cdn.example.test/rocket-cover.png", "", "https://cdn.example.test/rocket.mp4", 900), + }) + svc := New(Config{}, nil, nil, catalog) + svc.now = func() time.Time { return time.UnixMilli(1770000000000) } + svc.configs["lalu"] = &gamev1.RoomRPSConfig{ + AppCode: "lalu", + GameId: GameID, + Status: "active", + ChallengeTimeoutMs: 600000, + RevealCountdownMs: 3000, + StakeGifts: []*gamev1.RoomRPSStakeGift{ + {GiftIdText: "Gifi-3", GiftName: "Rocket", GiftIconUrl: "https://cdn.example.test/rocket.mp4", GiftPriceCoin: 900, Enabled: true, SortOrder: 1}, + {GiftId: 10002, Enabled: true, SortOrder: 2}, + {GiftId: 10003, Enabled: true, SortOrder: 3}, + {GiftId: 10004, Enabled: true, SortOrder: 4}, + }, + CreatedAtMs: 1000, + UpdatedAtMs: 2000, + } + + config, _, err := svc.GetConfig(context.Background(), "lalu") + if err != nil { + t.Fatalf("GetConfig() error = %v", err) + } + + if got := config.GetStakeGifts()[0]; got.GetGiftIconUrl() != "https://cdn.example.test/rocket-cover.png" || got.GetGiftAnimationUrl() != "https://cdn.example.test/rocket.mp4" { + t.Fatalf("legacy animated icon was not split into cover and animation fields: %+v", got) + } +} + func TestCreateChallengeMatchesStringGiftID(t *testing.T) { catalog := newFakeGiftCatalog(map[string]*walletv1.GiftConfig{ "Gifi-3": roomRPSWalletGift("Gifi-3", "Tiger", "https://cdn.example.test/tiger.png", "", "", 1), 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 6b993ee4..e5d9b92d 100644 --- a/services/gateway-service/internal/transport/http/gameapi/game_view.go +++ b/services/gateway-service/internal/transport/http/gameapi/game_view.go @@ -125,13 +125,14 @@ type roomRPSConfigItemData struct { } type roomRPSStakeGiftData struct { - GiftID string `json:"gift_id"` - GiftIDNumber int64 `json:"gift_id_number"` - GiftName string `json:"gift_name"` - GiftIconURL string `json:"gift_icon_url"` - GiftPriceCoin int64 `json:"gift_price_coin"` - Enabled bool `json:"enabled"` - SortOrder int32 `json:"sort_order"` + GiftID string `json:"gift_id"` + GiftIDNumber int64 `json:"gift_id_number"` + GiftName string `json:"gift_name"` + GiftIconURL string `json:"gift_icon_url"` + GiftAnimationURL string `json:"gift_animation_url"` + GiftPriceCoin int64 `json:"gift_price_coin"` + Enabled bool `json:"enabled"` + SortOrder int32 `json:"sort_order"` } type diceMatchItemData struct { @@ -333,13 +334,14 @@ func roomRPSConfigItemDataFromProto(config *gamev1.RoomRPSConfig) roomRPSConfigI for _, gift := range config.GetStakeGifts() { // 礼物 ID 以字符串为主,兼容数字字段;Flutter 展示、缓存和提交 body 都优先使用 gift_id。 gifts = append(gifts, roomRPSStakeGiftData{ - GiftID: roomRPSStakeGiftIDFromProto(gift), - GiftIDNumber: gift.GetGiftId(), - GiftName: gift.GetGiftName(), - GiftIconURL: gift.GetGiftIconUrl(), - GiftPriceCoin: gift.GetGiftPriceCoin(), - Enabled: gift.GetEnabled(), - SortOrder: gift.GetSortOrder(), + GiftID: roomRPSStakeGiftIDFromProto(gift), + GiftIDNumber: gift.GetGiftId(), + GiftName: gift.GetGiftName(), + GiftIconURL: gift.GetGiftIconUrl(), + GiftAnimationURL: gift.GetGiftAnimationUrl(), + GiftPriceCoin: gift.GetGiftPriceCoin(), + Enabled: gift.GetEnabled(), + SortOrder: gift.GetSortOrder(), }) } return roomRPSConfigItemData{ diff --git a/services/gateway-service/internal/transport/http/gameapi/game_view_test.go b/services/gateway-service/internal/transport/http/gameapi/game_view_test.go new file mode 100644 index 00000000..d0c114b6 --- /dev/null +++ b/services/gateway-service/internal/transport/http/gameapi/game_view_test.go @@ -0,0 +1,37 @@ +package gameapi + +import ( + "testing" + + gamev1 "hyapp.local/api/proto/game/v1" +) + +func TestRoomRPSConfigDataFromProtoIncludesGiftAnimationURL(t *testing.T) { + data := roomRPSConfigDataFromProto(&gamev1.RoomRPSConfigResponse{ + Config: &gamev1.RoomRPSConfig{ + AppCode: "lalu", + GameId: "room_rps", + Status: "active", + StakeGifts: []*gamev1.RoomRPSStakeGift{ + { + GiftIdText: "87", + GiftName: "彩蛋兔", + GiftIconUrl: "https://cdn.example.test/gifts/egg-rabbit.png", + GiftAnimationUrl: "https://cdn.example.test/gifts/egg-rabbit.mp4", + GiftPriceCoin: 40000, + Enabled: true, + SortOrder: 1, + }, + }, + }, + ServerTimeMs: 1770000000000, + }) + + if len(data.Config.StakeGifts) != 1 { + t.Fatalf("expected one stake gift, got %+v", data.Config.StakeGifts) + } + got := data.Config.StakeGifts[0] + if got.GiftIconURL != "https://cdn.example.test/gifts/egg-rabbit.png" || got.GiftAnimationURL != "https://cdn.example.test/gifts/egg-rabbit.mp4" { + t.Fatalf("room rps stake gift media fields mismatch: %+v", got) + } +}