fix: split room rps gift media urls

This commit is contained in:
zhx 2026-06-16 14:11:07 +08:00
parent d455d24016
commit 29b580f0ce
8 changed files with 194 additions and 58 deletions

View File

@ -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" +

View File

@ -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 {

View File

@ -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 层处理。

View File

@ -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",

View File

@ -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

View File

@ -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),

View File

@ -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{

View File

@ -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)
}
}