diff --git a/api/proto/events/room/v1/events.pb.go b/api/proto/events/room/v1/events.pb.go index adb88b4a..53d140dd 100644 --- a/api/proto/events/room/v1/events.pb.go +++ b/api/proto/events/room/v1/events.pb.go @@ -292,6 +292,7 @@ type RoomBackgroundChanged struct { ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"` BackgroundId int64 `protobuf:"varint,2,opt,name=background_id,json=backgroundId,proto3" json:"background_id,omitempty"` RoomBackgroundUrl string `protobuf:"bytes,3,opt,name=room_background_url,json=roomBackgroundUrl,proto3" json:"room_background_url,omitempty"` + Media *RoomMediaSnapshot `protobuf:"bytes,4,opt,name=media,proto3" json:"media,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -347,6 +348,389 @@ func (x *RoomBackgroundChanged) GetRoomBackgroundUrl() string { return "" } +func (x *RoomBackgroundChanged) GetMedia() *RoomMediaSnapshot { + if x != nil { + return x.Media + } + return nil +} + +// RoomMediaSnapshot 固化已通过 gateway 文件头检测和 room-service 权限校验的媒体事实。 +type RoomMediaSnapshot struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + ObjectKey string `protobuf:"bytes,2,opt,name=object_key,json=objectKey,proto3" json:"object_key,omitempty"` + ContentType string `protobuf:"bytes,3,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + Format string `protobuf:"bytes,4,opt,name=format,proto3" json:"format,omitempty"` + SizeBytes int64 `protobuf:"varint,5,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + Width int32 `protobuf:"varint,6,opt,name=width,proto3" json:"width,omitempty"` + Height int32 `protobuf:"varint,7,opt,name=height,proto3" json:"height,omitempty"` + Animated bool `protobuf:"varint,8,opt,name=animated,proto3" json:"animated,omitempty"` + FrameCount int32 `protobuf:"varint,9,opt,name=frame_count,json=frameCount,proto3" json:"frame_count,omitempty"` + DurationMs int64 `protobuf:"varint,10,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + PosterUrl string `protobuf:"bytes,11,opt,name=poster_url,json=posterUrl,proto3" json:"poster_url,omitempty"` + ThumbnailUrl string `protobuf:"bytes,12,opt,name=thumbnail_url,json=thumbnailUrl,proto3" json:"thumbnail_url,omitempty"` + Sha256 string `protobuf:"bytes,13,opt,name=sha256,proto3" json:"sha256,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoomMediaSnapshot) Reset() { + *x = RoomMediaSnapshot{} + mi := &file_proto_events_room_v1_events_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoomMediaSnapshot) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoomMediaSnapshot) ProtoMessage() {} + +func (x *RoomMediaSnapshot) ProtoReflect() protoreflect.Message { + mi := &file_proto_events_room_v1_events_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoomMediaSnapshot.ProtoReflect.Descriptor instead. +func (*RoomMediaSnapshot) Descriptor() ([]byte, []int) { + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{4} +} + +func (x *RoomMediaSnapshot) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *RoomMediaSnapshot) GetObjectKey() string { + if x != nil { + return x.ObjectKey + } + return "" +} + +func (x *RoomMediaSnapshot) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +func (x *RoomMediaSnapshot) GetFormat() string { + if x != nil { + return x.Format + } + return "" +} + +func (x *RoomMediaSnapshot) GetSizeBytes() int64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +func (x *RoomMediaSnapshot) GetWidth() int32 { + if x != nil { + return x.Width + } + return 0 +} + +func (x *RoomMediaSnapshot) GetHeight() int32 { + if x != nil { + return x.Height + } + return 0 +} + +func (x *RoomMediaSnapshot) GetAnimated() bool { + if x != nil { + return x.Animated + } + return false +} + +func (x *RoomMediaSnapshot) GetFrameCount() int32 { + if x != nil { + return x.FrameCount + } + return 0 +} + +func (x *RoomMediaSnapshot) GetDurationMs() int64 { + if x != nil { + return x.DurationMs + } + return 0 +} + +func (x *RoomMediaSnapshot) GetPosterUrl() string { + if x != nil { + return x.PosterUrl + } + return "" +} + +func (x *RoomMediaSnapshot) GetThumbnailUrl() string { + if x != nil { + return x.ThumbnailUrl + } + return "" +} + +func (x *RoomMediaSnapshot) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +// RoomDecorationChanged 表达房间级边框或彩色房名最终样式发生变化。 +type RoomDecorationChanged struct { + state protoimpl.MessageState `protogen:"open.v1"` + ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"` + DecorationType string `protobuf:"bytes,2,opt,name=decoration_type,json=decorationType,proto3" json:"decoration_type,omitempty"` + ResourceId int64 `protobuf:"varint,3,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` + ResourceCode string `protobuf:"bytes,4,opt,name=resource_code,json=resourceCode,proto3" json:"resource_code,omitempty"` + ResourceType string `protobuf:"bytes,5,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"` + Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` + AssetUrl string `protobuf:"bytes,7,opt,name=asset_url,json=assetUrl,proto3" json:"asset_url,omitempty"` + PreviewUrl string `protobuf:"bytes,8,opt,name=preview_url,json=previewUrl,proto3" json:"preview_url,omitempty"` + AnimationUrl string `protobuf:"bytes,9,opt,name=animation_url,json=animationUrl,proto3" json:"animation_url,omitempty"` + Format string `protobuf:"bytes,10,opt,name=format,proto3" json:"format,omitempty"` + MetadataJson string `protobuf:"bytes,11,opt,name=metadata_json,json=metadataJson,proto3" json:"metadata_json,omitempty"` + TextColors []string `protobuf:"bytes,12,rep,name=text_colors,json=textColors,proto3" json:"text_colors,omitempty"` + Stops []float64 `protobuf:"fixed64,13,rep,packed,name=stops,proto3" json:"stops,omitempty"` + AngleDegrees float64 `protobuf:"fixed64,14,opt,name=angle_degrees,json=angleDegrees,proto3" json:"angle_degrees,omitempty"` + EntitlementId string `protobuf:"bytes,15,opt,name=entitlement_id,json=entitlementId,proto3" json:"entitlement_id,omitempty"` + ExpiresAtMs int64 `protobuf:"varint,16,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoomDecorationChanged) Reset() { + *x = RoomDecorationChanged{} + mi := &file_proto_events_room_v1_events_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoomDecorationChanged) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoomDecorationChanged) ProtoMessage() {} + +func (x *RoomDecorationChanged) ProtoReflect() protoreflect.Message { + mi := &file_proto_events_room_v1_events_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoomDecorationChanged.ProtoReflect.Descriptor instead. +func (*RoomDecorationChanged) Descriptor() ([]byte, []int) { + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{5} +} + +func (x *RoomDecorationChanged) GetActorUserId() int64 { + if x != nil { + return x.ActorUserId + } + return 0 +} + +func (x *RoomDecorationChanged) GetDecorationType() string { + if x != nil { + return x.DecorationType + } + return "" +} + +func (x *RoomDecorationChanged) GetResourceId() int64 { + if x != nil { + return x.ResourceId + } + return 0 +} + +func (x *RoomDecorationChanged) GetResourceCode() string { + if x != nil { + return x.ResourceCode + } + return "" +} + +func (x *RoomDecorationChanged) GetResourceType() string { + if x != nil { + return x.ResourceType + } + return "" +} + +func (x *RoomDecorationChanged) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RoomDecorationChanged) GetAssetUrl() string { + if x != nil { + return x.AssetUrl + } + return "" +} + +func (x *RoomDecorationChanged) GetPreviewUrl() string { + if x != nil { + return x.PreviewUrl + } + return "" +} + +func (x *RoomDecorationChanged) GetAnimationUrl() string { + if x != nil { + return x.AnimationUrl + } + return "" +} + +func (x *RoomDecorationChanged) GetFormat() string { + if x != nil { + return x.Format + } + return "" +} + +func (x *RoomDecorationChanged) GetMetadataJson() string { + if x != nil { + return x.MetadataJson + } + return "" +} + +func (x *RoomDecorationChanged) GetTextColors() []string { + if x != nil { + return x.TextColors + } + return nil +} + +func (x *RoomDecorationChanged) GetStops() []float64 { + if x != nil { + return x.Stops + } + return nil +} + +func (x *RoomDecorationChanged) GetAngleDegrees() float64 { + if x != nil { + return x.AngleDegrees + } + return 0 +} + +func (x *RoomDecorationChanged) GetEntitlementId() string { + if x != nil { + return x.EntitlementId + } + return "" +} + +func (x *RoomDecorationChanged) GetExpiresAtMs() int64 { + if x != nil { + return x.ExpiresAtMs + } + return 0 +} + +// RoomImageMessageSent 是服务端权益校验后写入 outbox 的唯一房间图片消息事实。 +// message_id/event_id/command_id 都保持稳定,Flutter 以 event_id 或 message_id 去重 at-least-once 投递。 +type RoomImageMessageSent struct { + state protoimpl.MessageState `protogen:"open.v1"` + MessageId string `protobuf:"bytes,1,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"` + SenderUserId int64 `protobuf:"varint,3,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"` + Image *RoomMediaSnapshot `protobuf:"bytes,4,opt,name=image,proto3" json:"image,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoomImageMessageSent) Reset() { + *x = RoomImageMessageSent{} + mi := &file_proto_events_room_v1_events_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoomImageMessageSent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoomImageMessageSent) ProtoMessage() {} + +func (x *RoomImageMessageSent) ProtoReflect() protoreflect.Message { + mi := &file_proto_events_room_v1_events_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoomImageMessageSent.ProtoReflect.Descriptor instead. +func (*RoomImageMessageSent) Descriptor() ([]byte, []int) { + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{6} +} + +func (x *RoomImageMessageSent) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +func (x *RoomImageMessageSent) GetCommandId() string { + if x != nil { + return x.CommandId + } + return "" +} + +func (x *RoomImageMessageSent) GetSenderUserId() int64 { + if x != nil { + return x.SenderUserId + } + return 0 +} + +func (x *RoomImageMessageSent) GetImage() *RoomMediaSnapshot { + if x != nil { + return x.Image + } + return nil +} + // RoomUserJoined 表达用户业务态进房成功。 type RoomEntryVehicleSnapshot struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -365,7 +749,7 @@ type RoomEntryVehicleSnapshot struct { func (x *RoomEntryVehicleSnapshot) Reset() { *x = RoomEntryVehicleSnapshot{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[4] + mi := &file_proto_events_room_v1_events_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -377,7 +761,7 @@ func (x *RoomEntryVehicleSnapshot) String() string { func (*RoomEntryVehicleSnapshot) ProtoMessage() {} func (x *RoomEntryVehicleSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[4] + mi := &file_proto_events_room_v1_events_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -390,7 +774,7 @@ func (x *RoomEntryVehicleSnapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomEntryVehicleSnapshot.ProtoReflect.Descriptor instead. func (*RoomEntryVehicleSnapshot) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{4} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{7} } func (x *RoomEntryVehicleSnapshot) GetResourceId() int64 { @@ -483,7 +867,7 @@ type RoomUserJoined struct { func (x *RoomUserJoined) Reset() { *x = RoomUserJoined{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[5] + mi := &file_proto_events_room_v1_events_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -495,7 +879,7 @@ func (x *RoomUserJoined) String() string { func (*RoomUserJoined) ProtoMessage() {} func (x *RoomUserJoined) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[5] + mi := &file_proto_events_room_v1_events_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -508,7 +892,7 @@ func (x *RoomUserJoined) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomUserJoined.ProtoReflect.Descriptor instead. func (*RoomUserJoined) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{5} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{8} } func (x *RoomUserJoined) GetUserId() int64 { @@ -626,7 +1010,7 @@ type RoomUserLeft struct { func (x *RoomUserLeft) Reset() { *x = RoomUserLeft{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[6] + mi := &file_proto_events_room_v1_events_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -638,7 +1022,7 @@ func (x *RoomUserLeft) String() string { func (*RoomUserLeft) ProtoMessage() {} func (x *RoomUserLeft) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[6] + mi := &file_proto_events_room_v1_events_proto_msgTypes[9] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -651,7 +1035,7 @@ func (x *RoomUserLeft) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomUserLeft.ProtoReflect.Descriptor instead. func (*RoomUserLeft) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{6} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{9} } func (x *RoomUserLeft) GetUserId() int64 { @@ -672,7 +1056,7 @@ type RoomClosed struct { func (x *RoomClosed) Reset() { *x = RoomClosed{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[7] + mi := &file_proto_events_room_v1_events_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -684,7 +1068,7 @@ func (x *RoomClosed) String() string { func (*RoomClosed) ProtoMessage() {} func (x *RoomClosed) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[7] + mi := &file_proto_events_room_v1_events_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -697,7 +1081,7 @@ func (x *RoomClosed) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomClosed.ProtoReflect.Descriptor instead. func (*RoomClosed) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{7} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10} } func (x *RoomClosed) GetActorUserId() int64 { @@ -744,7 +1128,7 @@ type RoomMicChanged struct { func (x *RoomMicChanged) Reset() { *x = RoomMicChanged{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[8] + mi := &file_proto_events_room_v1_events_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -756,7 +1140,7 @@ func (x *RoomMicChanged) String() string { func (*RoomMicChanged) ProtoMessage() {} func (x *RoomMicChanged) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[8] + mi := &file_proto_events_room_v1_events_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -769,7 +1153,7 @@ func (x *RoomMicChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomMicChanged.ProtoReflect.Descriptor instead. func (*RoomMicChanged) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{8} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11} } func (x *RoomMicChanged) GetActorUserId() int64 { @@ -903,7 +1287,7 @@ type RoomMicSeatLocked struct { func (x *RoomMicSeatLocked) Reset() { *x = RoomMicSeatLocked{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[9] + mi := &file_proto_events_room_v1_events_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -915,7 +1299,7 @@ func (x *RoomMicSeatLocked) String() string { func (*RoomMicSeatLocked) ProtoMessage() {} func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[9] + mi := &file_proto_events_room_v1_events_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -928,7 +1312,7 @@ func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomMicSeatLocked.ProtoReflect.Descriptor instead. func (*RoomMicSeatLocked) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{9} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12} } func (x *RoomMicSeatLocked) GetActorUserId() int64 { @@ -963,7 +1347,7 @@ type RoomChatEnabledChanged struct { func (x *RoomChatEnabledChanged) Reset() { *x = RoomChatEnabledChanged{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[10] + mi := &file_proto_events_room_v1_events_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -975,7 +1359,7 @@ func (x *RoomChatEnabledChanged) String() string { func (*RoomChatEnabledChanged) ProtoMessage() {} func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[10] + mi := &file_proto_events_room_v1_events_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -988,7 +1372,7 @@ func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomChatEnabledChanged.ProtoReflect.Descriptor instead. func (*RoomChatEnabledChanged) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13} } func (x *RoomChatEnabledChanged) GetActorUserId() int64 { @@ -1018,7 +1402,7 @@ type RoomPasswordChanged struct { func (x *RoomPasswordChanged) Reset() { *x = RoomPasswordChanged{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[11] + mi := &file_proto_events_room_v1_events_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1030,7 +1414,7 @@ func (x *RoomPasswordChanged) String() string { func (*RoomPasswordChanged) ProtoMessage() {} func (x *RoomPasswordChanged) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[11] + mi := &file_proto_events_room_v1_events_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1043,7 +1427,7 @@ func (x *RoomPasswordChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomPasswordChanged.ProtoReflect.Descriptor instead. func (*RoomPasswordChanged) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14} } func (x *RoomPasswordChanged) GetActorUserId() int64 { @@ -1079,7 +1463,7 @@ type RoomAdminChanged struct { func (x *RoomAdminChanged) Reset() { *x = RoomAdminChanged{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[12] + mi := &file_proto_events_room_v1_events_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1091,7 +1475,7 @@ func (x *RoomAdminChanged) String() string { func (*RoomAdminChanged) ProtoMessage() {} func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[12] + mi := &file_proto_events_room_v1_events_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1104,7 +1488,7 @@ func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomAdminChanged.ProtoReflect.Descriptor instead. func (*RoomAdminChanged) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15} } func (x *RoomAdminChanged) GetActorUserId() int64 { @@ -1140,7 +1524,7 @@ type RoomUserMuted struct { func (x *RoomUserMuted) Reset() { *x = RoomUserMuted{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[13] + mi := &file_proto_events_room_v1_events_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1152,7 +1536,7 @@ func (x *RoomUserMuted) String() string { func (*RoomUserMuted) ProtoMessage() {} func (x *RoomUserMuted) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[13] + mi := &file_proto_events_room_v1_events_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1165,7 +1549,7 @@ func (x *RoomUserMuted) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomUserMuted.ProtoReflect.Descriptor instead. func (*RoomUserMuted) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{16} } func (x *RoomUserMuted) GetActorUserId() int64 { @@ -1200,7 +1584,7 @@ type RoomUserKicked struct { func (x *RoomUserKicked) Reset() { *x = RoomUserKicked{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[14] + mi := &file_proto_events_room_v1_events_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1212,7 +1596,7 @@ func (x *RoomUserKicked) String() string { func (*RoomUserKicked) ProtoMessage() {} func (x *RoomUserKicked) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[14] + mi := &file_proto_events_room_v1_events_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1225,7 +1609,7 @@ func (x *RoomUserKicked) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomUserKicked.ProtoReflect.Descriptor instead. func (*RoomUserKicked) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17} } func (x *RoomUserKicked) GetActorUserId() int64 { @@ -1253,7 +1637,7 @@ type RoomUserUnbanned struct { func (x *RoomUserUnbanned) Reset() { *x = RoomUserUnbanned{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[15] + mi := &file_proto_events_room_v1_events_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1265,7 +1649,7 @@ func (x *RoomUserUnbanned) String() string { func (*RoomUserUnbanned) ProtoMessage() {} func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[15] + mi := &file_proto_events_room_v1_events_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1278,7 +1662,7 @@ func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomUserUnbanned.ProtoReflect.Descriptor instead. func (*RoomUserUnbanned) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18} } func (x *RoomUserUnbanned) GetActorUserId() int64 { @@ -1341,7 +1725,7 @@ type RoomGiftSent struct { func (x *RoomGiftSent) Reset() { *x = RoomGiftSent{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[16] + mi := &file_proto_events_room_v1_events_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1353,7 +1737,7 @@ func (x *RoomGiftSent) String() string { func (*RoomGiftSent) ProtoMessage() {} func (x *RoomGiftSent) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[16] + mi := &file_proto_events_room_v1_events_proto_msgTypes[19] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1366,7 +1750,7 @@ func (x *RoomGiftSent) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomGiftSent.ProtoReflect.Descriptor instead. func (*RoomGiftSent) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{16} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19} } func (x *RoomGiftSent) GetSenderUserId() int64 { @@ -1612,7 +1996,7 @@ type RoomGiftBatchLuckyResult struct { func (x *RoomGiftBatchLuckyResult) Reset() { *x = RoomGiftBatchLuckyResult{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[17] + mi := &file_proto_events_room_v1_events_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1624,7 +2008,7 @@ func (x *RoomGiftBatchLuckyResult) String() string { func (*RoomGiftBatchLuckyResult) ProtoMessage() {} func (x *RoomGiftBatchLuckyResult) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[17] + mi := &file_proto_events_room_v1_events_proto_msgTypes[20] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1637,7 +2021,7 @@ func (x *RoomGiftBatchLuckyResult) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomGiftBatchLuckyResult.ProtoReflect.Descriptor instead. func (*RoomGiftBatchLuckyResult) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20} } func (x *RoomGiftBatchLuckyResult) GetEnabled() bool { @@ -1773,7 +2157,7 @@ type RoomGiftBatchTarget struct { func (x *RoomGiftBatchTarget) Reset() { *x = RoomGiftBatchTarget{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[18] + mi := &file_proto_events_room_v1_events_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1785,7 +2169,7 @@ func (x *RoomGiftBatchTarget) String() string { func (*RoomGiftBatchTarget) ProtoMessage() {} func (x *RoomGiftBatchTarget) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[18] + mi := &file_proto_events_room_v1_events_proto_msgTypes[21] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1798,7 +2182,7 @@ func (x *RoomGiftBatchTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomGiftBatchTarget.ProtoReflect.Descriptor instead. func (*RoomGiftBatchTarget) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21} } func (x *RoomGiftBatchTarget) GetTargetUserId() int64 { @@ -1910,7 +2294,7 @@ type RoomGiftBatchSent struct { func (x *RoomGiftBatchSent) Reset() { *x = RoomGiftBatchSent{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[19] + mi := &file_proto_events_room_v1_events_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1922,7 +2306,7 @@ func (x *RoomGiftBatchSent) String() string { func (*RoomGiftBatchSent) ProtoMessage() {} func (x *RoomGiftBatchSent) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[19] + mi := &file_proto_events_room_v1_events_proto_msgTypes[22] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1935,7 +2319,7 @@ func (x *RoomGiftBatchSent) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomGiftBatchSent.ProtoReflect.Descriptor instead. func (*RoomGiftBatchSent) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22} } func (x *RoomGiftBatchSent) GetSenderUserId() int64 { @@ -2129,7 +2513,7 @@ type RoomLuckyGiftDrawn struct { func (x *RoomLuckyGiftDrawn) Reset() { *x = RoomLuckyGiftDrawn{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[20] + mi := &file_proto_events_room_v1_events_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2141,7 +2525,7 @@ func (x *RoomLuckyGiftDrawn) String() string { func (*RoomLuckyGiftDrawn) ProtoMessage() {} func (x *RoomLuckyGiftDrawn) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[20] + mi := &file_proto_events_room_v1_events_proto_msgTypes[23] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2154,7 +2538,7 @@ func (x *RoomLuckyGiftDrawn) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomLuckyGiftDrawn.ProtoReflect.Descriptor instead. func (*RoomLuckyGiftDrawn) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{23} } func (x *RoomLuckyGiftDrawn) GetDrawId() string { @@ -2363,7 +2747,7 @@ type RoomRobotLuckyGiftDrawn struct { func (x *RoomRobotLuckyGiftDrawn) Reset() { *x = RoomRobotLuckyGiftDrawn{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[21] + mi := &file_proto_events_room_v1_events_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2375,7 +2759,7 @@ func (x *RoomRobotLuckyGiftDrawn) String() string { func (*RoomRobotLuckyGiftDrawn) ProtoMessage() {} func (x *RoomRobotLuckyGiftDrawn) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[21] + mi := &file_proto_events_room_v1_events_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2388,7 +2772,7 @@ func (x *RoomRobotLuckyGiftDrawn) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomRobotLuckyGiftDrawn.ProtoReflect.Descriptor instead. func (*RoomRobotLuckyGiftDrawn) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{24} } func (x *RoomRobotLuckyGiftDrawn) GetDrawId() string { @@ -2500,7 +2884,7 @@ type RoomHeatChanged struct { func (x *RoomHeatChanged) Reset() { *x = RoomHeatChanged{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[22] + mi := &file_proto_events_room_v1_events_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2512,7 +2896,7 @@ func (x *RoomHeatChanged) String() string { func (*RoomHeatChanged) ProtoMessage() {} func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[22] + mi := &file_proto_events_room_v1_events_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2525,7 +2909,7 @@ func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomHeatChanged.ProtoReflect.Descriptor instead. func (*RoomHeatChanged) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{25} } func (x *RoomHeatChanged) GetDelta() int64 { @@ -2554,7 +2938,7 @@ type RoomRankChanged struct { func (x *RoomRankChanged) Reset() { *x = RoomRankChanged{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[23] + mi := &file_proto_events_room_v1_events_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2566,7 +2950,7 @@ func (x *RoomRankChanged) String() string { func (*RoomRankChanged) ProtoMessage() {} func (x *RoomRankChanged) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[23] + mi := &file_proto_events_room_v1_events_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2579,7 +2963,7 @@ func (x *RoomRankChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomRankChanged.ProtoReflect.Descriptor instead. func (*RoomRankChanged) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{23} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{26} } func (x *RoomRankChanged) GetUserId() int64 { @@ -2619,7 +3003,7 @@ type RoomRocketRewardGrant struct { func (x *RoomRocketRewardGrant) Reset() { *x = RoomRocketRewardGrant{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[24] + mi := &file_proto_events_room_v1_events_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2631,7 +3015,7 @@ func (x *RoomRocketRewardGrant) String() string { func (*RoomRocketRewardGrant) ProtoMessage() {} func (x *RoomRocketRewardGrant) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[24] + mi := &file_proto_events_room_v1_events_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2644,7 +3028,7 @@ func (x *RoomRocketRewardGrant) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomRocketRewardGrant.ProtoReflect.Descriptor instead. func (*RoomRocketRewardGrant) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{24} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{27} } func (x *RoomRocketRewardGrant) GetRewardRole() string { @@ -2726,7 +3110,7 @@ type RoomRocketFuelChanged struct { func (x *RoomRocketFuelChanged) Reset() { *x = RoomRocketFuelChanged{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[25] + mi := &file_proto_events_room_v1_events_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2738,7 +3122,7 @@ func (x *RoomRocketFuelChanged) String() string { func (*RoomRocketFuelChanged) ProtoMessage() {} func (x *RoomRocketFuelChanged) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[25] + mi := &file_proto_events_room_v1_events_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2751,7 +3135,7 @@ func (x *RoomRocketFuelChanged) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomRocketFuelChanged.ProtoReflect.Descriptor instead. func (*RoomRocketFuelChanged) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{25} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{28} } func (x *RoomRocketFuelChanged) GetRocketId() string { @@ -2875,7 +3259,7 @@ type RoomRocketIgnited struct { func (x *RoomRocketIgnited) Reset() { *x = RoomRocketIgnited{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[26] + mi := &file_proto_events_room_v1_events_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2887,7 +3271,7 @@ func (x *RoomRocketIgnited) String() string { func (*RoomRocketIgnited) ProtoMessage() {} func (x *RoomRocketIgnited) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[26] + mi := &file_proto_events_room_v1_events_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2900,7 +3284,7 @@ func (x *RoomRocketIgnited) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomRocketIgnited.ProtoReflect.Descriptor instead. func (*RoomRocketIgnited) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{26} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{29} } func (x *RoomRocketIgnited) GetRocketId() string { @@ -3019,7 +3403,7 @@ type RoomRocketLaunched struct { func (x *RoomRocketLaunched) Reset() { *x = RoomRocketLaunched{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[27] + mi := &file_proto_events_room_v1_events_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3031,7 +3415,7 @@ func (x *RoomRocketLaunched) String() string { func (*RoomRocketLaunched) ProtoMessage() {} func (x *RoomRocketLaunched) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[27] + mi := &file_proto_events_room_v1_events_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3044,7 +3428,7 @@ func (x *RoomRocketLaunched) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomRocketLaunched.ProtoReflect.Descriptor instead. func (*RoomRocketLaunched) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{27} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{30} } func (x *RoomRocketLaunched) GetRocketId() string { @@ -3122,7 +3506,7 @@ type RoomRocketRewardGranted struct { func (x *RoomRocketRewardGranted) Reset() { *x = RoomRocketRewardGranted{} - mi := &file_proto_events_room_v1_events_proto_msgTypes[28] + mi := &file_proto_events_room_v1_events_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3134,7 +3518,7 @@ func (x *RoomRocketRewardGranted) String() string { func (*RoomRocketRewardGranted) ProtoMessage() {} func (x *RoomRocketRewardGranted) ProtoReflect() protoreflect.Message { - mi := &file_proto_events_room_v1_events_proto_msgTypes[28] + mi := &file_proto_events_room_v1_events_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3147,7 +3531,7 @@ func (x *RoomRocketRewardGranted) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomRocketRewardGranted.ProtoReflect.Descriptor instead. func (*RoomRocketRewardGranted) Descriptor() ([]byte, []int) { - return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{28} + return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{31} } func (x *RoomRocketRewardGranted) GetRocketId() string { @@ -3202,11 +3586,60 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" + "\x10room_description\x18\x04 \x01(\tR\x0froomDescription\x12\x1d\n" + "\n" + "seat_count\x18\x05 \x01(\x05R\tseatCount\x12.\n" + - "\x13room_background_url\x18\x06 \x01(\tR\x11roomBackgroundUrl\"\x90\x01\n" + + "\x13room_background_url\x18\x06 \x01(\tR\x11roomBackgroundUrl\"\xcf\x01\n" + "\x15RoomBackgroundChanged\x12\"\n" + "\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12#\n" + "\rbackground_id\x18\x02 \x01(\x03R\fbackgroundId\x12.\n" + - "\x13room_background_url\x18\x03 \x01(\tR\x11roomBackgroundUrl\"\xc7\x02\n" + + "\x13room_background_url\x18\x03 \x01(\tR\x11roomBackgroundUrl\x12=\n" + + "\x05media\x18\x04 \x01(\v2'.hyapp.events.room.v1.RoomMediaSnapshotR\x05media\"\x86\x03\n" + + "\x11RoomMediaSnapshot\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12\x1d\n" + + "\n" + + "object_key\x18\x02 \x01(\tR\tobjectKey\x12!\n" + + "\fcontent_type\x18\x03 \x01(\tR\vcontentType\x12\x16\n" + + "\x06format\x18\x04 \x01(\tR\x06format\x12\x1d\n" + + "\n" + + "size_bytes\x18\x05 \x01(\x03R\tsizeBytes\x12\x14\n" + + "\x05width\x18\x06 \x01(\x05R\x05width\x12\x16\n" + + "\x06height\x18\a \x01(\x05R\x06height\x12\x1a\n" + + "\banimated\x18\b \x01(\bR\banimated\x12\x1f\n" + + "\vframe_count\x18\t \x01(\x05R\n" + + "frameCount\x12\x1f\n" + + "\vduration_ms\x18\n" + + " \x01(\x03R\n" + + "durationMs\x12\x1d\n" + + "\n" + + "poster_url\x18\v \x01(\tR\tposterUrl\x12#\n" + + "\rthumbnail_url\x18\f \x01(\tR\fthumbnailUrl\x12\x16\n" + + "\x06sha256\x18\r \x01(\tR\x06sha256\"\xaa\x04\n" + + "\x15RoomDecorationChanged\x12\"\n" + + "\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12'\n" + + "\x0fdecoration_type\x18\x02 \x01(\tR\x0edecorationType\x12\x1f\n" + + "\vresource_id\x18\x03 \x01(\x03R\n" + + "resourceId\x12#\n" + + "\rresource_code\x18\x04 \x01(\tR\fresourceCode\x12#\n" + + "\rresource_type\x18\x05 \x01(\tR\fresourceType\x12\x12\n" + + "\x04name\x18\x06 \x01(\tR\x04name\x12\x1b\n" + + "\tasset_url\x18\a \x01(\tR\bassetUrl\x12\x1f\n" + + "\vpreview_url\x18\b \x01(\tR\n" + + "previewUrl\x12#\n" + + "\ranimation_url\x18\t \x01(\tR\fanimationUrl\x12\x16\n" + + "\x06format\x18\n" + + " \x01(\tR\x06format\x12#\n" + + "\rmetadata_json\x18\v \x01(\tR\fmetadataJson\x12\x1f\n" + + "\vtext_colors\x18\f \x03(\tR\n" + + "textColors\x12\x14\n" + + "\x05stops\x18\r \x03(\x01R\x05stops\x12#\n" + + "\rangle_degrees\x18\x0e \x01(\x01R\fangleDegrees\x12%\n" + + "\x0eentitlement_id\x18\x0f \x01(\tR\rentitlementId\x12\"\n" + + "\rexpires_at_ms\x18\x10 \x01(\x03R\vexpiresAtMs\"\xb9\x01\n" + + "\x14RoomImageMessageSent\x12\x1d\n" + + "\n" + + "message_id\x18\x01 \x01(\tR\tmessageId\x12\x1d\n" + + "\n" + + "command_id\x18\x02 \x01(\tR\tcommandId\x12$\n" + + "\x0esender_user_id\x18\x03 \x01(\x03R\fsenderUserId\x12=\n" + + "\x05image\x18\x04 \x01(\v2'.hyapp.events.room.v1.RoomMediaSnapshotR\x05image\"\xc7\x02\n" + "\x18RoomEntryVehicleSnapshot\x12\x1f\n" + "\vresource_id\x18\x01 \x01(\x03R\n" + "resourceId\x12#\n" + @@ -3526,49 +3959,54 @@ func file_proto_events_room_v1_events_proto_rawDescGZIP() []byte { return file_proto_events_room_v1_events_proto_rawDescData } -var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 29) +var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 32) var file_proto_events_room_v1_events_proto_goTypes = []any{ (*EventEnvelope)(nil), // 0: hyapp.events.room.v1.EventEnvelope (*RoomCreated)(nil), // 1: hyapp.events.room.v1.RoomCreated (*RoomProfileUpdated)(nil), // 2: hyapp.events.room.v1.RoomProfileUpdated (*RoomBackgroundChanged)(nil), // 3: hyapp.events.room.v1.RoomBackgroundChanged - (*RoomEntryVehicleSnapshot)(nil), // 4: hyapp.events.room.v1.RoomEntryVehicleSnapshot - (*RoomUserJoined)(nil), // 5: hyapp.events.room.v1.RoomUserJoined - (*RoomUserLeft)(nil), // 6: hyapp.events.room.v1.RoomUserLeft - (*RoomClosed)(nil), // 7: hyapp.events.room.v1.RoomClosed - (*RoomMicChanged)(nil), // 8: hyapp.events.room.v1.RoomMicChanged - (*RoomMicSeatLocked)(nil), // 9: hyapp.events.room.v1.RoomMicSeatLocked - (*RoomChatEnabledChanged)(nil), // 10: hyapp.events.room.v1.RoomChatEnabledChanged - (*RoomPasswordChanged)(nil), // 11: hyapp.events.room.v1.RoomPasswordChanged - (*RoomAdminChanged)(nil), // 12: hyapp.events.room.v1.RoomAdminChanged - (*RoomUserMuted)(nil), // 13: hyapp.events.room.v1.RoomUserMuted - (*RoomUserKicked)(nil), // 14: hyapp.events.room.v1.RoomUserKicked - (*RoomUserUnbanned)(nil), // 15: hyapp.events.room.v1.RoomUserUnbanned - (*RoomGiftSent)(nil), // 16: hyapp.events.room.v1.RoomGiftSent - (*RoomGiftBatchLuckyResult)(nil), // 17: hyapp.events.room.v1.RoomGiftBatchLuckyResult - (*RoomGiftBatchTarget)(nil), // 18: hyapp.events.room.v1.RoomGiftBatchTarget - (*RoomGiftBatchSent)(nil), // 19: hyapp.events.room.v1.RoomGiftBatchSent - (*RoomLuckyGiftDrawn)(nil), // 20: hyapp.events.room.v1.RoomLuckyGiftDrawn - (*RoomRobotLuckyGiftDrawn)(nil), // 21: hyapp.events.room.v1.RoomRobotLuckyGiftDrawn - (*RoomHeatChanged)(nil), // 22: hyapp.events.room.v1.RoomHeatChanged - (*RoomRankChanged)(nil), // 23: hyapp.events.room.v1.RoomRankChanged - (*RoomRocketRewardGrant)(nil), // 24: hyapp.events.room.v1.RoomRocketRewardGrant - (*RoomRocketFuelChanged)(nil), // 25: hyapp.events.room.v1.RoomRocketFuelChanged - (*RoomRocketIgnited)(nil), // 26: hyapp.events.room.v1.RoomRocketIgnited - (*RoomRocketLaunched)(nil), // 27: hyapp.events.room.v1.RoomRocketLaunched - (*RoomRocketRewardGranted)(nil), // 28: hyapp.events.room.v1.RoomRocketRewardGranted + (*RoomMediaSnapshot)(nil), // 4: hyapp.events.room.v1.RoomMediaSnapshot + (*RoomDecorationChanged)(nil), // 5: hyapp.events.room.v1.RoomDecorationChanged + (*RoomImageMessageSent)(nil), // 6: hyapp.events.room.v1.RoomImageMessageSent + (*RoomEntryVehicleSnapshot)(nil), // 7: hyapp.events.room.v1.RoomEntryVehicleSnapshot + (*RoomUserJoined)(nil), // 8: hyapp.events.room.v1.RoomUserJoined + (*RoomUserLeft)(nil), // 9: hyapp.events.room.v1.RoomUserLeft + (*RoomClosed)(nil), // 10: hyapp.events.room.v1.RoomClosed + (*RoomMicChanged)(nil), // 11: hyapp.events.room.v1.RoomMicChanged + (*RoomMicSeatLocked)(nil), // 12: hyapp.events.room.v1.RoomMicSeatLocked + (*RoomChatEnabledChanged)(nil), // 13: hyapp.events.room.v1.RoomChatEnabledChanged + (*RoomPasswordChanged)(nil), // 14: hyapp.events.room.v1.RoomPasswordChanged + (*RoomAdminChanged)(nil), // 15: hyapp.events.room.v1.RoomAdminChanged + (*RoomUserMuted)(nil), // 16: hyapp.events.room.v1.RoomUserMuted + (*RoomUserKicked)(nil), // 17: hyapp.events.room.v1.RoomUserKicked + (*RoomUserUnbanned)(nil), // 18: hyapp.events.room.v1.RoomUserUnbanned + (*RoomGiftSent)(nil), // 19: hyapp.events.room.v1.RoomGiftSent + (*RoomGiftBatchLuckyResult)(nil), // 20: hyapp.events.room.v1.RoomGiftBatchLuckyResult + (*RoomGiftBatchTarget)(nil), // 21: hyapp.events.room.v1.RoomGiftBatchTarget + (*RoomGiftBatchSent)(nil), // 22: hyapp.events.room.v1.RoomGiftBatchSent + (*RoomLuckyGiftDrawn)(nil), // 23: hyapp.events.room.v1.RoomLuckyGiftDrawn + (*RoomRobotLuckyGiftDrawn)(nil), // 24: hyapp.events.room.v1.RoomRobotLuckyGiftDrawn + (*RoomHeatChanged)(nil), // 25: hyapp.events.room.v1.RoomHeatChanged + (*RoomRankChanged)(nil), // 26: hyapp.events.room.v1.RoomRankChanged + (*RoomRocketRewardGrant)(nil), // 27: hyapp.events.room.v1.RoomRocketRewardGrant + (*RoomRocketFuelChanged)(nil), // 28: hyapp.events.room.v1.RoomRocketFuelChanged + (*RoomRocketIgnited)(nil), // 29: hyapp.events.room.v1.RoomRocketIgnited + (*RoomRocketLaunched)(nil), // 30: hyapp.events.room.v1.RoomRocketLaunched + (*RoomRocketRewardGranted)(nil), // 31: hyapp.events.room.v1.RoomRocketRewardGranted } var file_proto_events_room_v1_events_proto_depIdxs = []int32{ - 4, // 0: hyapp.events.room.v1.RoomUserJoined.entry_vehicle:type_name -> hyapp.events.room.v1.RoomEntryVehicleSnapshot - 17, // 1: hyapp.events.room.v1.RoomGiftBatchTarget.lucky_gift:type_name -> hyapp.events.room.v1.RoomGiftBatchLuckyResult - 18, // 2: hyapp.events.room.v1.RoomGiftBatchSent.targets:type_name -> hyapp.events.room.v1.RoomGiftBatchTarget - 24, // 3: hyapp.events.room.v1.RoomRocketLaunched.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant - 24, // 4: hyapp.events.room.v1.RoomRocketRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant - 5, // [5:5] is the sub-list for method output_type - 5, // [5:5] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 4, // 0: hyapp.events.room.v1.RoomBackgroundChanged.media:type_name -> hyapp.events.room.v1.RoomMediaSnapshot + 4, // 1: hyapp.events.room.v1.RoomImageMessageSent.image:type_name -> hyapp.events.room.v1.RoomMediaSnapshot + 7, // 2: hyapp.events.room.v1.RoomUserJoined.entry_vehicle:type_name -> hyapp.events.room.v1.RoomEntryVehicleSnapshot + 20, // 3: hyapp.events.room.v1.RoomGiftBatchTarget.lucky_gift:type_name -> hyapp.events.room.v1.RoomGiftBatchLuckyResult + 21, // 4: hyapp.events.room.v1.RoomGiftBatchSent.targets:type_name -> hyapp.events.room.v1.RoomGiftBatchTarget + 27, // 5: hyapp.events.room.v1.RoomRocketLaunched.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant + 27, // 6: hyapp.events.room.v1.RoomRocketRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name } func init() { file_proto_events_room_v1_events_proto_init() } @@ -3582,7 +4020,7 @@ func file_proto_events_room_v1_events_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_events_room_v1_events_proto_rawDesc), len(file_proto_events_room_v1_events_proto_rawDesc)), NumEnums: 0, - NumMessages: 29, + NumMessages: 32, NumExtensions: 0, NumServices: 0, }, diff --git a/api/proto/events/room/v1/events.proto b/api/proto/events/room/v1/events.proto index 17aea2c5..767c0f93 100644 --- a/api/proto/events/room/v1/events.proto +++ b/api/proto/events/room/v1/events.proto @@ -42,6 +42,53 @@ message RoomBackgroundChanged { int64 actor_user_id = 1; int64 background_id = 2; string room_background_url = 3; + RoomMediaSnapshot media = 4; +} + +// RoomMediaSnapshot 固化已通过 gateway 文件头检测和 room-service 权限校验的媒体事实。 +message RoomMediaSnapshot { + string url = 1; + string object_key = 2; + string content_type = 3; + string format = 4; + int64 size_bytes = 5; + int32 width = 6; + int32 height = 7; + bool animated = 8; + int32 frame_count = 9; + int64 duration_ms = 10; + string poster_url = 11; + string thumbnail_url = 12; + string sha256 = 13; +} + +// RoomDecorationChanged 表达房间级边框或彩色房名最终样式发生变化。 +message RoomDecorationChanged { + int64 actor_user_id = 1; + string decoration_type = 2; + int64 resource_id = 3; + string resource_code = 4; + string resource_type = 5; + string name = 6; + string asset_url = 7; + string preview_url = 8; + string animation_url = 9; + string format = 10; + string metadata_json = 11; + repeated string text_colors = 12; + repeated double stops = 13; + double angle_degrees = 14; + string entitlement_id = 15; + int64 expires_at_ms = 16; +} + +// RoomImageMessageSent 是服务端权益校验后写入 outbox 的唯一房间图片消息事实。 +// message_id/event_id/command_id 都保持稳定,Flutter 以 event_id 或 message_id 去重 at-least-once 投递。 +message RoomImageMessageSent { + string message_id = 1; + string command_id = 2; + int64 sender_user_id = 3; + RoomMediaSnapshot image = 4; } // RoomUserJoined 表达用户业务态进房成功。 diff --git a/api/proto/luckygift/v1/luckygift.pb.go b/api/proto/luckygift/v1/luckygift.pb.go index 593a8a28..62f4817d 100644 --- a/api/proto/luckygift/v1/luckygift.pb.go +++ b/api/proto/luckygift/v1/luckygift.pb.go @@ -2030,6 +2030,224 @@ func (x *ListLuckyGiftConfigsResponse) GetConfigs() []*LuckyGiftRuleConfig { return nil } +// ListLuckyGiftConfigVersions 只读取同一 app+pool 的不可变历史快照;返回顺序为版本号倒序。 +type ListLuckyGiftConfigVersionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListLuckyGiftConfigVersionsRequest) Reset() { + *x = ListLuckyGiftConfigVersionsRequest{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListLuckyGiftConfigVersionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListLuckyGiftConfigVersionsRequest) ProtoMessage() {} + +func (x *ListLuckyGiftConfigVersionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListLuckyGiftConfigVersionsRequest.ProtoReflect.Descriptor instead. +func (*ListLuckyGiftConfigVersionsRequest) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{20} +} + +func (x *ListLuckyGiftConfigVersionsRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *ListLuckyGiftConfigVersionsRequest) GetPoolId() string { + if x != nil { + return x.PoolId + } + return "" +} + +type ListLuckyGiftConfigVersionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Configs []*LuckyGiftRuleConfig `protobuf:"bytes,1,rep,name=configs,proto3" json:"configs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListLuckyGiftConfigVersionsResponse) Reset() { + *x = ListLuckyGiftConfigVersionsResponse{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListLuckyGiftConfigVersionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListLuckyGiftConfigVersionsResponse) ProtoMessage() {} + +func (x *ListLuckyGiftConfigVersionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListLuckyGiftConfigVersionsResponse.ProtoReflect.Descriptor instead. +func (*ListLuckyGiftConfigVersionsResponse) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{21} +} + +func (x *ListLuckyGiftConfigVersionsResponse) GetConfigs() []*LuckyGiftRuleConfig { + if x != nil { + return x.Configs + } + return nil +} + +// RollbackLuckyGiftConfig 不覆盖历史行,而是把目标快照复制成一个新的最新版本并立即生效。 +type RollbackLuckyGiftConfigRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"` + TargetRuleVersion int64 `protobuf:"varint,3,opt,name=target_rule_version,json=targetRuleVersion,proto3" json:"target_rule_version,omitempty"` + OperatorAdminId int64 `protobuf:"varint,4,opt,name=operator_admin_id,json=operatorAdminId,proto3" json:"operator_admin_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RollbackLuckyGiftConfigRequest) Reset() { + *x = RollbackLuckyGiftConfigRequest{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RollbackLuckyGiftConfigRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RollbackLuckyGiftConfigRequest) ProtoMessage() {} + +func (x *RollbackLuckyGiftConfigRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RollbackLuckyGiftConfigRequest.ProtoReflect.Descriptor instead. +func (*RollbackLuckyGiftConfigRequest) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{22} +} + +func (x *RollbackLuckyGiftConfigRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *RollbackLuckyGiftConfigRequest) GetPoolId() string { + if x != nil { + return x.PoolId + } + return "" +} + +func (x *RollbackLuckyGiftConfigRequest) GetTargetRuleVersion() int64 { + if x != nil { + return x.TargetRuleVersion + } + return 0 +} + +func (x *RollbackLuckyGiftConfigRequest) GetOperatorAdminId() int64 { + if x != nil { + return x.OperatorAdminId + } + return 0 +} + +type RollbackLuckyGiftConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + SourceRuleVersion int64 `protobuf:"varint,1,opt,name=source_rule_version,json=sourceRuleVersion,proto3" json:"source_rule_version,omitempty"` + Config *LuckyGiftRuleConfig `protobuf:"bytes,2,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RollbackLuckyGiftConfigResponse) Reset() { + *x = RollbackLuckyGiftConfigResponse{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RollbackLuckyGiftConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RollbackLuckyGiftConfigResponse) ProtoMessage() {} + +func (x *RollbackLuckyGiftConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RollbackLuckyGiftConfigResponse.ProtoReflect.Descriptor instead. +func (*RollbackLuckyGiftConfigResponse) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{23} +} + +func (x *RollbackLuckyGiftConfigResponse) GetSourceRuleVersion() int64 { + if x != nil { + return x.SourceRuleVersion + } + return 0 +} + +func (x *RollbackLuckyGiftConfigResponse) GetConfig() *LuckyGiftRuleConfig { + if x != nil { + return x.Config + } + return nil +} + type ListLuckyGiftDrawsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` @@ -2048,7 +2266,7 @@ type ListLuckyGiftDrawsRequest struct { func (x *ListLuckyGiftDrawsRequest) Reset() { *x = ListLuckyGiftDrawsRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[20] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2060,7 +2278,7 @@ func (x *ListLuckyGiftDrawsRequest) String() string { func (*ListLuckyGiftDrawsRequest) ProtoMessage() {} func (x *ListLuckyGiftDrawsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[20] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[24] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2073,7 +2291,7 @@ func (x *ListLuckyGiftDrawsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLuckyGiftDrawsRequest.ProtoReflect.Descriptor instead. func (*ListLuckyGiftDrawsRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{20} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{24} } func (x *ListLuckyGiftDrawsRequest) GetMeta() *RequestMeta { @@ -2156,7 +2374,7 @@ type ListLuckyGiftDrawsResponse struct { func (x *ListLuckyGiftDrawsResponse) Reset() { *x = ListLuckyGiftDrawsResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[21] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2168,7 +2386,7 @@ func (x *ListLuckyGiftDrawsResponse) String() string { func (*ListLuckyGiftDrawsResponse) ProtoMessage() {} func (x *ListLuckyGiftDrawsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[21] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[25] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2181,7 +2399,7 @@ func (x *ListLuckyGiftDrawsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLuckyGiftDrawsResponse.ProtoReflect.Descriptor instead. func (*ListLuckyGiftDrawsResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{21} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{25} } func (x *ListLuckyGiftDrawsResponse) GetDraws() []*LuckyGiftDrawResult { @@ -2217,7 +2435,7 @@ type LuckyGiftDrawSummary struct { func (x *LuckyGiftDrawSummary) Reset() { *x = LuckyGiftDrawSummary{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[22] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2229,7 +2447,7 @@ func (x *LuckyGiftDrawSummary) String() string { func (*LuckyGiftDrawSummary) ProtoMessage() {} func (x *LuckyGiftDrawSummary) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[22] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[26] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2242,7 +2460,7 @@ func (x *LuckyGiftDrawSummary) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftDrawSummary.ProtoReflect.Descriptor instead. func (*LuckyGiftDrawSummary) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{22} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{26} } func (x *LuckyGiftDrawSummary) GetPoolId() string { @@ -2342,7 +2560,7 @@ type LuckyGiftPoolBalance struct { func (x *LuckyGiftPoolBalance) Reset() { *x = LuckyGiftPoolBalance{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[23] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2354,7 +2572,7 @@ func (x *LuckyGiftPoolBalance) String() string { func (*LuckyGiftPoolBalance) ProtoMessage() {} func (x *LuckyGiftPoolBalance) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[23] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[27] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2367,7 +2585,7 @@ func (x *LuckyGiftPoolBalance) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftPoolBalance.ProtoReflect.Descriptor instead. func (*LuckyGiftPoolBalance) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{23} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{27} } func (x *LuckyGiftPoolBalance) GetAppCode() string { @@ -2470,7 +2688,7 @@ type GetLuckyGiftDrawSummaryRequest struct { func (x *GetLuckyGiftDrawSummaryRequest) Reset() { *x = GetLuckyGiftDrawSummaryRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[24] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2482,7 +2700,7 @@ func (x *GetLuckyGiftDrawSummaryRequest) String() string { func (*GetLuckyGiftDrawSummaryRequest) ProtoMessage() {} func (x *GetLuckyGiftDrawSummaryRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[24] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[28] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2495,7 +2713,7 @@ func (x *GetLuckyGiftDrawSummaryRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLuckyGiftDrawSummaryRequest.ProtoReflect.Descriptor instead. func (*GetLuckyGiftDrawSummaryRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{24} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{28} } func (x *GetLuckyGiftDrawSummaryRequest) GetMeta() *RequestMeta { @@ -2563,7 +2781,7 @@ type GetLuckyGiftDrawSummaryResponse struct { func (x *GetLuckyGiftDrawSummaryResponse) Reset() { *x = GetLuckyGiftDrawSummaryResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[25] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2575,7 +2793,7 @@ func (x *GetLuckyGiftDrawSummaryResponse) String() string { func (*GetLuckyGiftDrawSummaryResponse) ProtoMessage() {} func (x *GetLuckyGiftDrawSummaryResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[25] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[29] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2588,7 +2806,7 @@ func (x *GetLuckyGiftDrawSummaryResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLuckyGiftDrawSummaryResponse.ProtoReflect.Descriptor instead. func (*GetLuckyGiftDrawSummaryResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{25} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{29} } func (x *GetLuckyGiftDrawSummaryResponse) GetSummary() *LuckyGiftDrawSummary { @@ -2609,7 +2827,7 @@ type ListLuckyGiftPoolBalancesRequest struct { func (x *ListLuckyGiftPoolBalancesRequest) Reset() { *x = ListLuckyGiftPoolBalancesRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[26] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2621,7 +2839,7 @@ func (x *ListLuckyGiftPoolBalancesRequest) String() string { func (*ListLuckyGiftPoolBalancesRequest) ProtoMessage() {} func (x *ListLuckyGiftPoolBalancesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[26] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[30] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2634,7 +2852,7 @@ func (x *ListLuckyGiftPoolBalancesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLuckyGiftPoolBalancesRequest.ProtoReflect.Descriptor instead. func (*ListLuckyGiftPoolBalancesRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{26} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{30} } func (x *ListLuckyGiftPoolBalancesRequest) GetMeta() *RequestMeta { @@ -2667,7 +2885,7 @@ type ListLuckyGiftPoolBalancesResponse struct { func (x *ListLuckyGiftPoolBalancesResponse) Reset() { *x = ListLuckyGiftPoolBalancesResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[27] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2679,7 +2897,7 @@ func (x *ListLuckyGiftPoolBalancesResponse) String() string { func (*ListLuckyGiftPoolBalancesResponse) ProtoMessage() {} func (x *ListLuckyGiftPoolBalancesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[27] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[31] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2692,7 +2910,7 @@ func (x *ListLuckyGiftPoolBalancesResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListLuckyGiftPoolBalancesResponse.ProtoReflect.Descriptor instead. func (*ListLuckyGiftPoolBalancesResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{27} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{31} } func (x *ListLuckyGiftPoolBalancesResponse) GetPools() []*LuckyGiftPoolBalance { @@ -2714,7 +2932,7 @@ type LuckyGiftExperimentOverride struct { func (x *LuckyGiftExperimentOverride) Reset() { *x = LuckyGiftExperimentOverride{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[28] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[32] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2726,7 +2944,7 @@ func (x *LuckyGiftExperimentOverride) String() string { func (*LuckyGiftExperimentOverride) ProtoMessage() {} func (x *LuckyGiftExperimentOverride) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[28] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[32] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2739,7 +2957,7 @@ func (x *LuckyGiftExperimentOverride) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftExperimentOverride.ProtoReflect.Descriptor instead. func (*LuckyGiftExperimentOverride) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{28} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{32} } func (x *LuckyGiftExperimentOverride) GetUserKey() string { @@ -2773,7 +2991,7 @@ type LuckyGiftExperimentArmStats struct { func (x *LuckyGiftExperimentArmStats) Reset() { *x = LuckyGiftExperimentArmStats{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[29] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[33] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2785,7 +3003,7 @@ func (x *LuckyGiftExperimentArmStats) String() string { func (*LuckyGiftExperimentArmStats) ProtoMessage() {} func (x *LuckyGiftExperimentArmStats) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[29] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[33] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2798,7 +3016,7 @@ func (x *LuckyGiftExperimentArmStats) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftExperimentArmStats.ProtoReflect.Descriptor instead. func (*LuckyGiftExperimentArmStats) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{29} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{33} } func (x *LuckyGiftExperimentArmStats) GetArm() string { @@ -2888,7 +3106,7 @@ type LuckyGiftExperiment struct { func (x *LuckyGiftExperiment) Reset() { *x = LuckyGiftExperiment{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[30] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[34] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2900,7 +3118,7 @@ func (x *LuckyGiftExperiment) String() string { func (*LuckyGiftExperiment) ProtoMessage() {} func (x *LuckyGiftExperiment) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[30] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[34] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2913,7 +3131,7 @@ func (x *LuckyGiftExperiment) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftExperiment.ProtoReflect.Descriptor instead. func (*LuckyGiftExperiment) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{30} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{34} } func (x *LuckyGiftExperiment) GetAppCode() string { @@ -3059,7 +3277,7 @@ type CreateLuckyGiftExperimentRequest struct { func (x *CreateLuckyGiftExperimentRequest) Reset() { *x = CreateLuckyGiftExperimentRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[31] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[35] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3071,7 +3289,7 @@ func (x *CreateLuckyGiftExperimentRequest) String() string { func (*CreateLuckyGiftExperimentRequest) ProtoMessage() {} func (x *CreateLuckyGiftExperimentRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[31] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[35] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3084,7 +3302,7 @@ func (x *CreateLuckyGiftExperimentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateLuckyGiftExperimentRequest.ProtoReflect.Descriptor instead. func (*CreateLuckyGiftExperimentRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{31} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{35} } func (x *CreateLuckyGiftExperimentRequest) GetMeta() *RequestMeta { @@ -3139,7 +3357,7 @@ type CreateLuckyGiftExperimentResponse struct { func (x *CreateLuckyGiftExperimentResponse) Reset() { *x = CreateLuckyGiftExperimentResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[32] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[36] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3151,7 +3369,7 @@ func (x *CreateLuckyGiftExperimentResponse) String() string { func (*CreateLuckyGiftExperimentResponse) ProtoMessage() {} func (x *CreateLuckyGiftExperimentResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[32] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[36] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3164,7 +3382,7 @@ func (x *CreateLuckyGiftExperimentResponse) ProtoReflect() protoreflect.Message // Deprecated: Use CreateLuckyGiftExperimentResponse.ProtoReflect.Descriptor instead. func (*CreateLuckyGiftExperimentResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{32} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{36} } func (x *CreateLuckyGiftExperimentResponse) GetExperiment() *LuckyGiftExperiment { @@ -3193,7 +3411,7 @@ type ListLuckyGiftExperimentsRequest struct { func (x *ListLuckyGiftExperimentsRequest) Reset() { *x = ListLuckyGiftExperimentsRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[33] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[37] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3205,7 +3423,7 @@ func (x *ListLuckyGiftExperimentsRequest) String() string { func (*ListLuckyGiftExperimentsRequest) ProtoMessage() {} func (x *ListLuckyGiftExperimentsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[33] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[37] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3218,7 +3436,7 @@ func (x *ListLuckyGiftExperimentsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLuckyGiftExperimentsRequest.ProtoReflect.Descriptor instead. func (*ListLuckyGiftExperimentsRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{33} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{37} } func (x *ListLuckyGiftExperimentsRequest) GetMeta() *RequestMeta { @@ -3258,7 +3476,7 @@ type ListLuckyGiftExperimentsResponse struct { func (x *ListLuckyGiftExperimentsResponse) Reset() { *x = ListLuckyGiftExperimentsResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[34] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[38] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3270,7 +3488,7 @@ func (x *ListLuckyGiftExperimentsResponse) String() string { func (*ListLuckyGiftExperimentsResponse) ProtoMessage() {} func (x *ListLuckyGiftExperimentsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[34] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[38] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3283,7 +3501,7 @@ func (x *ListLuckyGiftExperimentsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLuckyGiftExperimentsResponse.ProtoReflect.Descriptor instead. func (*ListLuckyGiftExperimentsResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{34} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{38} } func (x *ListLuckyGiftExperimentsResponse) GetExperiments() []*LuckyGiftExperiment { @@ -3309,7 +3527,7 @@ type UpdateLuckyGiftExperimentRequest struct { func (x *UpdateLuckyGiftExperimentRequest) Reset() { *x = UpdateLuckyGiftExperimentRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[35] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[39] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3321,7 +3539,7 @@ func (x *UpdateLuckyGiftExperimentRequest) String() string { func (*UpdateLuckyGiftExperimentRequest) ProtoMessage() {} func (x *UpdateLuckyGiftExperimentRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[35] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[39] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3334,7 +3552,7 @@ func (x *UpdateLuckyGiftExperimentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateLuckyGiftExperimentRequest.ProtoReflect.Descriptor instead. func (*UpdateLuckyGiftExperimentRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{35} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{39} } func (x *UpdateLuckyGiftExperimentRequest) GetMeta() *RequestMeta { @@ -3381,7 +3599,7 @@ type UpdateLuckyGiftExperimentResponse struct { func (x *UpdateLuckyGiftExperimentResponse) Reset() { *x = UpdateLuckyGiftExperimentResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[36] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[40] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3393,7 +3611,7 @@ func (x *UpdateLuckyGiftExperimentResponse) String() string { func (*UpdateLuckyGiftExperimentResponse) ProtoMessage() {} func (x *UpdateLuckyGiftExperimentResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[36] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[40] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3406,7 +3624,7 @@ func (x *UpdateLuckyGiftExperimentResponse) ProtoReflect() protoreflect.Message // Deprecated: Use UpdateLuckyGiftExperimentResponse.ProtoReflect.Descriptor instead. func (*UpdateLuckyGiftExperimentResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{36} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{40} } func (x *UpdateLuckyGiftExperimentResponse) GetExperiment() *LuckyGiftExperiment { @@ -3429,7 +3647,7 @@ type SetLuckyGiftExperimentOverridesRequest struct { func (x *SetLuckyGiftExperimentOverridesRequest) Reset() { *x = SetLuckyGiftExperimentOverridesRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[37] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[41] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3441,7 +3659,7 @@ func (x *SetLuckyGiftExperimentOverridesRequest) String() string { func (*SetLuckyGiftExperimentOverridesRequest) ProtoMessage() {} func (x *SetLuckyGiftExperimentOverridesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[37] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[41] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3454,7 +3672,7 @@ func (x *SetLuckyGiftExperimentOverridesRequest) ProtoReflect() protoreflect.Mes // Deprecated: Use SetLuckyGiftExperimentOverridesRequest.ProtoReflect.Descriptor instead. func (*SetLuckyGiftExperimentOverridesRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{37} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{41} } func (x *SetLuckyGiftExperimentOverridesRequest) GetMeta() *RequestMeta { @@ -3501,7 +3719,7 @@ type SetLuckyGiftExperimentOverridesResponse struct { func (x *SetLuckyGiftExperimentOverridesResponse) Reset() { *x = SetLuckyGiftExperimentOverridesResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[38] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[42] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3513,7 +3731,7 @@ func (x *SetLuckyGiftExperimentOverridesResponse) String() string { func (*SetLuckyGiftExperimentOverridesResponse) ProtoMessage() {} func (x *SetLuckyGiftExperimentOverridesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[38] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[42] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3526,7 +3744,7 @@ func (x *SetLuckyGiftExperimentOverridesResponse) ProtoReflect() protoreflect.Me // Deprecated: Use SetLuckyGiftExperimentOverridesResponse.ProtoReflect.Descriptor instead. func (*SetLuckyGiftExperimentOverridesResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{38} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{42} } func (x *SetLuckyGiftExperimentOverridesResponse) GetExperiment() *LuckyGiftExperiment { @@ -3550,7 +3768,7 @@ type EndLuckyGiftExperimentRequest struct { func (x *EndLuckyGiftExperimentRequest) Reset() { *x = EndLuckyGiftExperimentRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[39] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[43] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3562,7 +3780,7 @@ func (x *EndLuckyGiftExperimentRequest) String() string { func (*EndLuckyGiftExperimentRequest) ProtoMessage() {} func (x *EndLuckyGiftExperimentRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[39] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[43] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3575,7 +3793,7 @@ func (x *EndLuckyGiftExperimentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use EndLuckyGiftExperimentRequest.ProtoReflect.Descriptor instead. func (*EndLuckyGiftExperimentRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{39} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{43} } func (x *EndLuckyGiftExperimentRequest) GetMeta() *RequestMeta { @@ -3616,7 +3834,7 @@ type EndLuckyGiftExperimentResponse struct { func (x *EndLuckyGiftExperimentResponse) Reset() { *x = EndLuckyGiftExperimentResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[40] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[44] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3628,7 +3846,7 @@ func (x *EndLuckyGiftExperimentResponse) String() string { func (*EndLuckyGiftExperimentResponse) ProtoMessage() {} func (x *EndLuckyGiftExperimentResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[40] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[44] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3641,7 +3859,7 @@ func (x *EndLuckyGiftExperimentResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use EndLuckyGiftExperimentResponse.ProtoReflect.Descriptor instead. func (*EndLuckyGiftExperimentResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{40} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{44} } func (x *EndLuckyGiftExperimentResponse) GetExperiment() *LuckyGiftExperiment { @@ -3673,7 +3891,7 @@ type LuckyGiftHit struct { func (x *LuckyGiftHit) Reset() { *x = LuckyGiftHit{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[41] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[45] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3685,7 +3903,7 @@ func (x *LuckyGiftHit) String() string { func (*LuckyGiftHit) ProtoMessage() {} func (x *LuckyGiftHit) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[41] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[45] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3698,7 +3916,7 @@ func (x *LuckyGiftHit) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftHit.ProtoReflect.Descriptor instead. func (*LuckyGiftHit) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{41} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{45} } func (x *LuckyGiftHit) GetGiftIndex() int32 { @@ -3754,7 +3972,7 @@ type AdjustLuckyGiftPoolBalanceRequest struct { func (x *AdjustLuckyGiftPoolBalanceRequest) Reset() { *x = AdjustLuckyGiftPoolBalanceRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[42] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[46] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3766,7 +3984,7 @@ func (x *AdjustLuckyGiftPoolBalanceRequest) String() string { func (*AdjustLuckyGiftPoolBalanceRequest) ProtoMessage() {} func (x *AdjustLuckyGiftPoolBalanceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[42] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[46] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3779,7 +3997,7 @@ func (x *AdjustLuckyGiftPoolBalanceRequest) ProtoReflect() protoreflect.Message // Deprecated: Use AdjustLuckyGiftPoolBalanceRequest.ProtoReflect.Descriptor instead. func (*AdjustLuckyGiftPoolBalanceRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{42} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{46} } func (x *AdjustLuckyGiftPoolBalanceRequest) GetMeta() *RequestMeta { @@ -3857,7 +4075,7 @@ type LuckyGiftPoolAdjustment struct { func (x *LuckyGiftPoolAdjustment) Reset() { *x = LuckyGiftPoolAdjustment{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[43] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[47] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3869,7 +4087,7 @@ func (x *LuckyGiftPoolAdjustment) String() string { func (*LuckyGiftPoolAdjustment) ProtoMessage() {} func (x *LuckyGiftPoolAdjustment) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[43] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[47] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3882,7 +4100,7 @@ func (x *LuckyGiftPoolAdjustment) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftPoolAdjustment.ProtoReflect.Descriptor instead. func (*LuckyGiftPoolAdjustment) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{43} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{47} } func (x *LuckyGiftPoolAdjustment) GetAdjustmentId() string { @@ -3973,7 +4191,7 @@ type AdjustLuckyGiftPoolBalanceResponse struct { func (x *AdjustLuckyGiftPoolBalanceResponse) Reset() { *x = AdjustLuckyGiftPoolBalanceResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[44] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[48] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -3985,7 +4203,7 @@ func (x *AdjustLuckyGiftPoolBalanceResponse) String() string { func (*AdjustLuckyGiftPoolBalanceResponse) ProtoMessage() {} func (x *AdjustLuckyGiftPoolBalanceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[44] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[48] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -3998,7 +4216,7 @@ func (x *AdjustLuckyGiftPoolBalanceResponse) ProtoReflect() protoreflect.Message // Deprecated: Use AdjustLuckyGiftPoolBalanceResponse.ProtoReflect.Descriptor instead. func (*AdjustLuckyGiftPoolBalanceResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{44} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{48} } func (x *AdjustLuckyGiftPoolBalanceResponse) GetAdjustment() *LuckyGiftPoolAdjustment { @@ -4038,7 +4256,7 @@ type LuckyGiftUserProfileWindow struct { func (x *LuckyGiftUserProfileWindow) Reset() { *x = LuckyGiftUserProfileWindow{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[45] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[49] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4050,7 +4268,7 @@ func (x *LuckyGiftUserProfileWindow) String() string { func (*LuckyGiftUserProfileWindow) ProtoMessage() {} func (x *LuckyGiftUserProfileWindow) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[45] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[49] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4063,7 +4281,7 @@ func (x *LuckyGiftUserProfileWindow) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftUserProfileWindow.ProtoReflect.Descriptor instead. func (*LuckyGiftUserProfileWindow) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{45} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{49} } func (x *LuckyGiftUserProfileWindow) GetDraws() int64 { @@ -4152,7 +4370,7 @@ type LuckyGiftUserProfile struct { func (x *LuckyGiftUserProfile) Reset() { *x = LuckyGiftUserProfile{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[46] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[50] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4164,7 +4382,7 @@ func (x *LuckyGiftUserProfile) String() string { func (*LuckyGiftUserProfile) ProtoMessage() {} func (x *LuckyGiftUserProfile) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[46] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[50] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4177,7 +4395,7 @@ func (x *LuckyGiftUserProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftUserProfile.ProtoReflect.Descriptor instead. func (*LuckyGiftUserProfile) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{46} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{50} } func (x *LuckyGiftUserProfile) GetAppCode() string { @@ -4453,7 +4671,7 @@ type ListLuckyGiftUserProfilesRequest struct { func (x *ListLuckyGiftUserProfilesRequest) Reset() { *x = ListLuckyGiftUserProfilesRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[47] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[51] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4465,7 +4683,7 @@ func (x *ListLuckyGiftUserProfilesRequest) String() string { func (*ListLuckyGiftUserProfilesRequest) ProtoMessage() {} func (x *ListLuckyGiftUserProfilesRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[47] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[51] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4478,7 +4696,7 @@ func (x *ListLuckyGiftUserProfilesRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLuckyGiftUserProfilesRequest.ProtoReflect.Descriptor instead. func (*ListLuckyGiftUserProfilesRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{47} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{51} } func (x *ListLuckyGiftUserProfilesRequest) GetMeta() *RequestMeta { @@ -4578,7 +4796,7 @@ type ListLuckyGiftUserProfilesResponse struct { func (x *ListLuckyGiftUserProfilesResponse) Reset() { *x = ListLuckyGiftUserProfilesResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[48] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[52] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4590,7 +4808,7 @@ func (x *ListLuckyGiftUserProfilesResponse) String() string { func (*ListLuckyGiftUserProfilesResponse) ProtoMessage() {} func (x *ListLuckyGiftUserProfilesResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[48] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[52] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4603,7 +4821,7 @@ func (x *ListLuckyGiftUserProfilesResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ListLuckyGiftUserProfilesResponse.ProtoReflect.Descriptor instead. func (*ListLuckyGiftUserProfilesResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{48} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{52} } func (x *ListLuckyGiftUserProfilesResponse) GetProfiles() []*LuckyGiftUserProfile { @@ -4658,7 +4876,7 @@ type GetLuckyGiftUserProfileRequest struct { func (x *GetLuckyGiftUserProfileRequest) Reset() { *x = GetLuckyGiftUserProfileRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[49] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[53] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4670,7 +4888,7 @@ func (x *GetLuckyGiftUserProfileRequest) String() string { func (*GetLuckyGiftUserProfileRequest) ProtoMessage() {} func (x *GetLuckyGiftUserProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[49] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[53] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4683,7 +4901,7 @@ func (x *GetLuckyGiftUserProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLuckyGiftUserProfileRequest.ProtoReflect.Descriptor instead. func (*GetLuckyGiftUserProfileRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{49} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{53} } func (x *GetLuckyGiftUserProfileRequest) GetMeta() *RequestMeta { @@ -4755,7 +4973,7 @@ type LuckyGiftUserMultiplierStat struct { func (x *LuckyGiftUserMultiplierStat) Reset() { *x = LuckyGiftUserMultiplierStat{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[50] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[54] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4767,7 +4985,7 @@ func (x *LuckyGiftUserMultiplierStat) String() string { func (*LuckyGiftUserMultiplierStat) ProtoMessage() {} func (x *LuckyGiftUserMultiplierStat) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[50] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[54] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4780,7 +4998,7 @@ func (x *LuckyGiftUserMultiplierStat) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftUserMultiplierStat.ProtoReflect.Descriptor instead. func (*LuckyGiftUserMultiplierStat) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{50} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{54} } func (x *LuckyGiftUserMultiplierStat) GetHitType() string { @@ -4834,7 +5052,7 @@ type LuckyGiftUserJackpotCondition struct { func (x *LuckyGiftUserJackpotCondition) Reset() { *x = LuckyGiftUserJackpotCondition{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[51] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[55] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4846,7 +5064,7 @@ func (x *LuckyGiftUserJackpotCondition) String() string { func (*LuckyGiftUserJackpotCondition) ProtoMessage() {} func (x *LuckyGiftUserJackpotCondition) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[51] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[55] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4859,7 +5077,7 @@ func (x *LuckyGiftUserJackpotCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftUserJackpotCondition.ProtoReflect.Descriptor instead. func (*LuckyGiftUserJackpotCondition) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{51} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{55} } func (x *LuckyGiftUserJackpotCondition) GetName() string { @@ -4941,7 +5159,7 @@ type LuckyGiftUserJackpotHit struct { func (x *LuckyGiftUserJackpotHit) Reset() { *x = LuckyGiftUserJackpotHit{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[52] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4953,7 +5171,7 @@ func (x *LuckyGiftUserJackpotHit) String() string { func (*LuckyGiftUserJackpotHit) ProtoMessage() {} func (x *LuckyGiftUserJackpotHit) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[52] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4966,7 +5184,7 @@ func (x *LuckyGiftUserJackpotHit) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftUserJackpotHit.ProtoReflect.Descriptor instead. func (*LuckyGiftUserJackpotHit) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{52} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{56} } func (x *LuckyGiftUserJackpotHit) GetDrawId() string { @@ -5084,7 +5302,7 @@ type GetLuckyGiftUserProfileResponse struct { func (x *GetLuckyGiftUserProfileResponse) Reset() { *x = GetLuckyGiftUserProfileResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[53] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5096,7 +5314,7 @@ func (x *GetLuckyGiftUserProfileResponse) String() string { func (*GetLuckyGiftUserProfileResponse) ProtoMessage() {} func (x *GetLuckyGiftUserProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[53] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5109,7 +5327,7 @@ func (x *GetLuckyGiftUserProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetLuckyGiftUserProfileResponse.ProtoReflect.Descriptor instead. func (*GetLuckyGiftUserProfileResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{53} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{57} } func (x *GetLuckyGiftUserProfileResponse) GetProfile() *LuckyGiftUserProfile { @@ -5196,7 +5414,7 @@ type ListLuckyGiftDailyStatsRequest struct { func (x *ListLuckyGiftDailyStatsRequest) Reset() { *x = ListLuckyGiftDailyStatsRequest{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[54] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5208,7 +5426,7 @@ func (x *ListLuckyGiftDailyStatsRequest) String() string { func (*ListLuckyGiftDailyStatsRequest) ProtoMessage() {} func (x *ListLuckyGiftDailyStatsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[54] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5221,7 +5439,7 @@ func (x *ListLuckyGiftDailyStatsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLuckyGiftDailyStatsRequest.ProtoReflect.Descriptor instead. func (*ListLuckyGiftDailyStatsRequest) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{54} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{58} } func (x *ListLuckyGiftDailyStatsRequest) GetMeta() *RequestMeta { @@ -5281,7 +5499,7 @@ type LuckyGiftDailyStat struct { func (x *LuckyGiftDailyStat) Reset() { *x = LuckyGiftDailyStat{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[55] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5293,7 +5511,7 @@ func (x *LuckyGiftDailyStat) String() string { func (*LuckyGiftDailyStat) ProtoMessage() {} func (x *LuckyGiftDailyStat) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[55] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5306,7 +5524,7 @@ func (x *LuckyGiftDailyStat) ProtoReflect() protoreflect.Message { // Deprecated: Use LuckyGiftDailyStat.ProtoReflect.Descriptor instead. func (*LuckyGiftDailyStat) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{55} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{59} } func (x *LuckyGiftDailyStat) GetStatDay() string { @@ -5353,7 +5571,7 @@ type ListLuckyGiftDailyStatsResponse struct { func (x *ListLuckyGiftDailyStatsResponse) Reset() { *x = ListLuckyGiftDailyStatsResponse{} - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[56] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5365,7 +5583,7 @@ func (x *ListLuckyGiftDailyStatsResponse) String() string { func (*ListLuckyGiftDailyStatsResponse) ProtoMessage() {} func (x *ListLuckyGiftDailyStatsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[56] + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5378,7 +5596,7 @@ func (x *ListLuckyGiftDailyStatsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListLuckyGiftDailyStatsResponse.ProtoReflect.Descriptor instead. func (*ListLuckyGiftDailyStatsResponse) Descriptor() ([]byte, []int) { - return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{56} + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{60} } func (x *ListLuckyGiftDailyStatsResponse) GetStats() []*LuckyGiftDailyStat { @@ -5594,7 +5812,20 @@ const file_proto_luckygift_v1_luckygift_proto_rawDesc = "" + "\x1bListLuckyGiftConfigsRequest\x123\n" + "\x04meta\x18\x01 \x01(\v2\x1f.hyapp.luckygift.v1.RequestMetaR\x04meta\"a\n" + "\x1cListLuckyGiftConfigsResponse\x12A\n" + - "\aconfigs\x18\x01 \x03(\v2'.hyapp.luckygift.v1.LuckyGiftRuleConfigR\aconfigs\"\xcc\x02\n" + + "\aconfigs\x18\x01 \x03(\v2'.hyapp.luckygift.v1.LuckyGiftRuleConfigR\aconfigs\"r\n" + + "\"ListLuckyGiftConfigVersionsRequest\x123\n" + + "\x04meta\x18\x01 \x01(\v2\x1f.hyapp.luckygift.v1.RequestMetaR\x04meta\x12\x17\n" + + "\apool_id\x18\x02 \x01(\tR\x06poolId\"h\n" + + "#ListLuckyGiftConfigVersionsResponse\x12A\n" + + "\aconfigs\x18\x01 \x03(\v2'.hyapp.luckygift.v1.LuckyGiftRuleConfigR\aconfigs\"\xca\x01\n" + + "\x1eRollbackLuckyGiftConfigRequest\x123\n" + + "\x04meta\x18\x01 \x01(\v2\x1f.hyapp.luckygift.v1.RequestMetaR\x04meta\x12\x17\n" + + "\apool_id\x18\x02 \x01(\tR\x06poolId\x12.\n" + + "\x13target_rule_version\x18\x03 \x01(\x03R\x11targetRuleVersion\x12*\n" + + "\x11operator_admin_id\x18\x04 \x01(\x03R\x0foperatorAdminId\"\x92\x01\n" + + "\x1fRollbackLuckyGiftConfigResponse\x12.\n" + + "\x13source_rule_version\x18\x01 \x01(\x03R\x11sourceRuleVersion\x12?\n" + + "\x06config\x18\x02 \x01(\v2'.hyapp.luckygift.v1.LuckyGiftRuleConfigR\x06config\"\xcc\x02\n" + "\x19ListLuckyGiftDrawsRequest\x123\n" + "\x04meta\x18\x01 \x01(\v2\x1f.hyapp.luckygift.v1.RequestMetaR\x04meta\x12\x17\n" + "\agift_id\x18\x02 \x01(\tR\x06giftId\x12\x17\n" + @@ -5926,11 +6157,13 @@ const file_proto_luckygift_v1_luckygift_proto_rawDesc = "" + "\x0eCheckLuckyGift\x12).hyapp.luckygift.v1.CheckLuckyGiftRequest\x1a*.hyapp.luckygift.v1.CheckLuckyGiftResponse\x12y\n" + "\x14ExecuteLuckyGiftDraw\x12/.hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest\x1a0.hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse\x12\x88\x01\n" + "\x19BatchExecuteLuckyGiftDraw\x124.hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest\x1a5.hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawResponse\x12t\n" + - "\x17ExecuteExternalGiftDraw\x12+.hyapp.luckygift.v1.ExternalGiftDrawRequest\x1a,.hyapp.luckygift.v1.ExternalGiftDrawResponse2\xe9\x0f\n" + + "\x17ExecuteExternalGiftDraw\x12+.hyapp.luckygift.v1.ExternalGiftDrawRequest\x1a,.hyapp.luckygift.v1.ExternalGiftDrawResponse2\xff\x11\n" + "\x15AdminLuckyGiftService\x12s\n" + "\x12GetLuckyGiftConfig\x12-.hyapp.luckygift.v1.GetLuckyGiftConfigRequest\x1a..hyapp.luckygift.v1.GetLuckyGiftConfigResponse\x12|\n" + "\x15UpsertLuckyGiftConfig\x120.hyapp.luckygift.v1.UpsertLuckyGiftConfigRequest\x1a1.hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse\x12y\n" + - "\x14ListLuckyGiftConfigs\x12/.hyapp.luckygift.v1.ListLuckyGiftConfigsRequest\x1a0.hyapp.luckygift.v1.ListLuckyGiftConfigsResponse\x12s\n" + + "\x14ListLuckyGiftConfigs\x12/.hyapp.luckygift.v1.ListLuckyGiftConfigsRequest\x1a0.hyapp.luckygift.v1.ListLuckyGiftConfigsResponse\x12\x8e\x01\n" + + "\x1bListLuckyGiftConfigVersions\x126.hyapp.luckygift.v1.ListLuckyGiftConfigVersionsRequest\x1a7.hyapp.luckygift.v1.ListLuckyGiftConfigVersionsResponse\x12\x82\x01\n" + + "\x17RollbackLuckyGiftConfig\x122.hyapp.luckygift.v1.RollbackLuckyGiftConfigRequest\x1a3.hyapp.luckygift.v1.RollbackLuckyGiftConfigResponse\x12s\n" + "\x12ListLuckyGiftDraws\x12-.hyapp.luckygift.v1.ListLuckyGiftDrawsRequest\x1a..hyapp.luckygift.v1.ListLuckyGiftDrawsResponse\x12\x82\x01\n" + "\x17GetLuckyGiftDrawSummary\x122.hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest\x1a3.hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse\x12\x88\x01\n" + "\x19ListLuckyGiftPoolBalances\x124.hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest\x1a5.hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse\x12\x8b\x01\n" + @@ -5956,7 +6189,7 @@ func file_proto_luckygift_v1_luckygift_proto_rawDescGZIP() []byte { return file_proto_luckygift_v1_luckygift_proto_rawDescData } -var file_proto_luckygift_v1_luckygift_proto_msgTypes = make([]protoimpl.MessageInfo, 57) +var file_proto_luckygift_v1_luckygift_proto_msgTypes = make([]protoimpl.MessageInfo, 61) var file_proto_luckygift_v1_luckygift_proto_goTypes = []any{ (*RequestMeta)(nil), // 0: hyapp.luckygift.v1.RequestMeta (*LuckyGiftMeta)(nil), // 1: hyapp.luckygift.v1.LuckyGiftMeta @@ -5978,50 +6211,54 @@ var file_proto_luckygift_v1_luckygift_proto_goTypes = []any{ (*UpsertLuckyGiftConfigResponse)(nil), // 17: hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse (*ListLuckyGiftConfigsRequest)(nil), // 18: hyapp.luckygift.v1.ListLuckyGiftConfigsRequest (*ListLuckyGiftConfigsResponse)(nil), // 19: hyapp.luckygift.v1.ListLuckyGiftConfigsResponse - (*ListLuckyGiftDrawsRequest)(nil), // 20: hyapp.luckygift.v1.ListLuckyGiftDrawsRequest - (*ListLuckyGiftDrawsResponse)(nil), // 21: hyapp.luckygift.v1.ListLuckyGiftDrawsResponse - (*LuckyGiftDrawSummary)(nil), // 22: hyapp.luckygift.v1.LuckyGiftDrawSummary - (*LuckyGiftPoolBalance)(nil), // 23: hyapp.luckygift.v1.LuckyGiftPoolBalance - (*GetLuckyGiftDrawSummaryRequest)(nil), // 24: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest - (*GetLuckyGiftDrawSummaryResponse)(nil), // 25: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse - (*ListLuckyGiftPoolBalancesRequest)(nil), // 26: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest - (*ListLuckyGiftPoolBalancesResponse)(nil), // 27: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse - (*LuckyGiftExperimentOverride)(nil), // 28: hyapp.luckygift.v1.LuckyGiftExperimentOverride - (*LuckyGiftExperimentArmStats)(nil), // 29: hyapp.luckygift.v1.LuckyGiftExperimentArmStats - (*LuckyGiftExperiment)(nil), // 30: hyapp.luckygift.v1.LuckyGiftExperiment - (*CreateLuckyGiftExperimentRequest)(nil), // 31: hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest - (*CreateLuckyGiftExperimentResponse)(nil), // 32: hyapp.luckygift.v1.CreateLuckyGiftExperimentResponse - (*ListLuckyGiftExperimentsRequest)(nil), // 33: hyapp.luckygift.v1.ListLuckyGiftExperimentsRequest - (*ListLuckyGiftExperimentsResponse)(nil), // 34: hyapp.luckygift.v1.ListLuckyGiftExperimentsResponse - (*UpdateLuckyGiftExperimentRequest)(nil), // 35: hyapp.luckygift.v1.UpdateLuckyGiftExperimentRequest - (*UpdateLuckyGiftExperimentResponse)(nil), // 36: hyapp.luckygift.v1.UpdateLuckyGiftExperimentResponse - (*SetLuckyGiftExperimentOverridesRequest)(nil), // 37: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesRequest - (*SetLuckyGiftExperimentOverridesResponse)(nil), // 38: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesResponse - (*EndLuckyGiftExperimentRequest)(nil), // 39: hyapp.luckygift.v1.EndLuckyGiftExperimentRequest - (*EndLuckyGiftExperimentResponse)(nil), // 40: hyapp.luckygift.v1.EndLuckyGiftExperimentResponse - (*LuckyGiftHit)(nil), // 41: hyapp.luckygift.v1.LuckyGiftHit - (*AdjustLuckyGiftPoolBalanceRequest)(nil), // 42: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest - (*LuckyGiftPoolAdjustment)(nil), // 43: hyapp.luckygift.v1.LuckyGiftPoolAdjustment - (*AdjustLuckyGiftPoolBalanceResponse)(nil), // 44: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse - (*LuckyGiftUserProfileWindow)(nil), // 45: hyapp.luckygift.v1.LuckyGiftUserProfileWindow - (*LuckyGiftUserProfile)(nil), // 46: hyapp.luckygift.v1.LuckyGiftUserProfile - (*ListLuckyGiftUserProfilesRequest)(nil), // 47: hyapp.luckygift.v1.ListLuckyGiftUserProfilesRequest - (*ListLuckyGiftUserProfilesResponse)(nil), // 48: hyapp.luckygift.v1.ListLuckyGiftUserProfilesResponse - (*GetLuckyGiftUserProfileRequest)(nil), // 49: hyapp.luckygift.v1.GetLuckyGiftUserProfileRequest - (*LuckyGiftUserMultiplierStat)(nil), // 50: hyapp.luckygift.v1.LuckyGiftUserMultiplierStat - (*LuckyGiftUserJackpotCondition)(nil), // 51: hyapp.luckygift.v1.LuckyGiftUserJackpotCondition - (*LuckyGiftUserJackpotHit)(nil), // 52: hyapp.luckygift.v1.LuckyGiftUserJackpotHit - (*GetLuckyGiftUserProfileResponse)(nil), // 53: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse - (*ListLuckyGiftDailyStatsRequest)(nil), // 54: hyapp.luckygift.v1.ListLuckyGiftDailyStatsRequest - (*LuckyGiftDailyStat)(nil), // 55: hyapp.luckygift.v1.LuckyGiftDailyStat - (*ListLuckyGiftDailyStatsResponse)(nil), // 56: hyapp.luckygift.v1.ListLuckyGiftDailyStatsResponse + (*ListLuckyGiftConfigVersionsRequest)(nil), // 20: hyapp.luckygift.v1.ListLuckyGiftConfigVersionsRequest + (*ListLuckyGiftConfigVersionsResponse)(nil), // 21: hyapp.luckygift.v1.ListLuckyGiftConfigVersionsResponse + (*RollbackLuckyGiftConfigRequest)(nil), // 22: hyapp.luckygift.v1.RollbackLuckyGiftConfigRequest + (*RollbackLuckyGiftConfigResponse)(nil), // 23: hyapp.luckygift.v1.RollbackLuckyGiftConfigResponse + (*ListLuckyGiftDrawsRequest)(nil), // 24: hyapp.luckygift.v1.ListLuckyGiftDrawsRequest + (*ListLuckyGiftDrawsResponse)(nil), // 25: hyapp.luckygift.v1.ListLuckyGiftDrawsResponse + (*LuckyGiftDrawSummary)(nil), // 26: hyapp.luckygift.v1.LuckyGiftDrawSummary + (*LuckyGiftPoolBalance)(nil), // 27: hyapp.luckygift.v1.LuckyGiftPoolBalance + (*GetLuckyGiftDrawSummaryRequest)(nil), // 28: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest + (*GetLuckyGiftDrawSummaryResponse)(nil), // 29: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse + (*ListLuckyGiftPoolBalancesRequest)(nil), // 30: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest + (*ListLuckyGiftPoolBalancesResponse)(nil), // 31: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse + (*LuckyGiftExperimentOverride)(nil), // 32: hyapp.luckygift.v1.LuckyGiftExperimentOverride + (*LuckyGiftExperimentArmStats)(nil), // 33: hyapp.luckygift.v1.LuckyGiftExperimentArmStats + (*LuckyGiftExperiment)(nil), // 34: hyapp.luckygift.v1.LuckyGiftExperiment + (*CreateLuckyGiftExperimentRequest)(nil), // 35: hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest + (*CreateLuckyGiftExperimentResponse)(nil), // 36: hyapp.luckygift.v1.CreateLuckyGiftExperimentResponse + (*ListLuckyGiftExperimentsRequest)(nil), // 37: hyapp.luckygift.v1.ListLuckyGiftExperimentsRequest + (*ListLuckyGiftExperimentsResponse)(nil), // 38: hyapp.luckygift.v1.ListLuckyGiftExperimentsResponse + (*UpdateLuckyGiftExperimentRequest)(nil), // 39: hyapp.luckygift.v1.UpdateLuckyGiftExperimentRequest + (*UpdateLuckyGiftExperimentResponse)(nil), // 40: hyapp.luckygift.v1.UpdateLuckyGiftExperimentResponse + (*SetLuckyGiftExperimentOverridesRequest)(nil), // 41: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesRequest + (*SetLuckyGiftExperimentOverridesResponse)(nil), // 42: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesResponse + (*EndLuckyGiftExperimentRequest)(nil), // 43: hyapp.luckygift.v1.EndLuckyGiftExperimentRequest + (*EndLuckyGiftExperimentResponse)(nil), // 44: hyapp.luckygift.v1.EndLuckyGiftExperimentResponse + (*LuckyGiftHit)(nil), // 45: hyapp.luckygift.v1.LuckyGiftHit + (*AdjustLuckyGiftPoolBalanceRequest)(nil), // 46: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest + (*LuckyGiftPoolAdjustment)(nil), // 47: hyapp.luckygift.v1.LuckyGiftPoolAdjustment + (*AdjustLuckyGiftPoolBalanceResponse)(nil), // 48: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse + (*LuckyGiftUserProfileWindow)(nil), // 49: hyapp.luckygift.v1.LuckyGiftUserProfileWindow + (*LuckyGiftUserProfile)(nil), // 50: hyapp.luckygift.v1.LuckyGiftUserProfile + (*ListLuckyGiftUserProfilesRequest)(nil), // 51: hyapp.luckygift.v1.ListLuckyGiftUserProfilesRequest + (*ListLuckyGiftUserProfilesResponse)(nil), // 52: hyapp.luckygift.v1.ListLuckyGiftUserProfilesResponse + (*GetLuckyGiftUserProfileRequest)(nil), // 53: hyapp.luckygift.v1.GetLuckyGiftUserProfileRequest + (*LuckyGiftUserMultiplierStat)(nil), // 54: hyapp.luckygift.v1.LuckyGiftUserMultiplierStat + (*LuckyGiftUserJackpotCondition)(nil), // 55: hyapp.luckygift.v1.LuckyGiftUserJackpotCondition + (*LuckyGiftUserJackpotHit)(nil), // 56: hyapp.luckygift.v1.LuckyGiftUserJackpotHit + (*GetLuckyGiftUserProfileResponse)(nil), // 57: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse + (*ListLuckyGiftDailyStatsRequest)(nil), // 58: hyapp.luckygift.v1.ListLuckyGiftDailyStatsRequest + (*LuckyGiftDailyStat)(nil), // 59: hyapp.luckygift.v1.LuckyGiftDailyStat + (*ListLuckyGiftDailyStatsResponse)(nil), // 60: hyapp.luckygift.v1.ListLuckyGiftDailyStatsResponse } var file_proto_luckygift_v1_luckygift_proto_depIdxs = []int32{ 0, // 0: hyapp.luckygift.v1.LuckyGiftMeta.meta:type_name -> hyapp.luckygift.v1.RequestMeta 2, // 1: hyapp.luckygift.v1.LuckyGiftRuleStage.tiers:type_name -> hyapp.luckygift.v1.LuckyGiftRuleTier 3, // 2: hyapp.luckygift.v1.LuckyGiftRuleConfig.stages:type_name -> hyapp.luckygift.v1.LuckyGiftRuleStage 0, // 3: hyapp.luckygift.v1.CheckLuckyGiftRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 41, // 4: hyapp.luckygift.v1.LuckyGiftDrawResult.hits:type_name -> hyapp.luckygift.v1.LuckyGiftHit + 45, // 4: hyapp.luckygift.v1.LuckyGiftDrawResult.hits:type_name -> hyapp.luckygift.v1.LuckyGiftHit 1, // 5: hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest.lucky_gift:type_name -> hyapp.luckygift.v1.LuckyGiftMeta 7, // 6: hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse.result:type_name -> hyapp.luckygift.v1.LuckyGiftDrawResult 1, // 7: hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest.lucky_gifts:type_name -> hyapp.luckygift.v1.LuckyGiftMeta @@ -6034,88 +6271,96 @@ var file_proto_luckygift_v1_luckygift_proto_depIdxs = []int32{ 4, // 14: hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse.config:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig 0, // 15: hyapp.luckygift.v1.ListLuckyGiftConfigsRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta 4, // 16: hyapp.luckygift.v1.ListLuckyGiftConfigsResponse.configs:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig - 0, // 17: hyapp.luckygift.v1.ListLuckyGiftDrawsRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 7, // 18: hyapp.luckygift.v1.ListLuckyGiftDrawsResponse.draws:type_name -> hyapp.luckygift.v1.LuckyGiftDrawResult - 0, // 19: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 22, // 20: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse.summary:type_name -> hyapp.luckygift.v1.LuckyGiftDrawSummary - 0, // 21: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 23, // 22: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse.pools:type_name -> hyapp.luckygift.v1.LuckyGiftPoolBalance - 28, // 23: hyapp.luckygift.v1.LuckyGiftExperiment.overrides:type_name -> hyapp.luckygift.v1.LuckyGiftExperimentOverride - 29, // 24: hyapp.luckygift.v1.LuckyGiftExperiment.arm_stats:type_name -> hyapp.luckygift.v1.LuckyGiftExperimentArmStats - 0, // 25: hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 4, // 26: hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest.treatment_config:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig - 28, // 27: hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest.overrides:type_name -> hyapp.luckygift.v1.LuckyGiftExperimentOverride - 30, // 28: hyapp.luckygift.v1.CreateLuckyGiftExperimentResponse.experiment:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment - 4, // 29: hyapp.luckygift.v1.CreateLuckyGiftExperimentResponse.treatment_config:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig - 0, // 30: hyapp.luckygift.v1.ListLuckyGiftExperimentsRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 30, // 31: hyapp.luckygift.v1.ListLuckyGiftExperimentsResponse.experiments:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment - 0, // 32: hyapp.luckygift.v1.UpdateLuckyGiftExperimentRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 30, // 33: hyapp.luckygift.v1.UpdateLuckyGiftExperimentResponse.experiment:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment - 0, // 34: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 28, // 35: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesRequest.upserts:type_name -> hyapp.luckygift.v1.LuckyGiftExperimentOverride - 30, // 36: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesResponse.experiment:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment - 0, // 37: hyapp.luckygift.v1.EndLuckyGiftExperimentRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 30, // 38: hyapp.luckygift.v1.EndLuckyGiftExperimentResponse.experiment:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment - 4, // 39: hyapp.luckygift.v1.EndLuckyGiftExperimentResponse.final_config:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig - 0, // 40: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 43, // 41: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse.adjustment:type_name -> hyapp.luckygift.v1.LuckyGiftPoolAdjustment - 23, // 42: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse.pool:type_name -> hyapp.luckygift.v1.LuckyGiftPoolBalance - 45, // 43: hyapp.luckygift.v1.LuckyGiftUserProfile.one_hour:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow - 45, // 44: hyapp.luckygift.v1.LuckyGiftUserProfile.twenty_four_hours:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow - 45, // 45: hyapp.luckygift.v1.LuckyGiftUserProfile.forty_eight_hours:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow - 45, // 46: hyapp.luckygift.v1.LuckyGiftUserProfile.lifetime:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow - 0, // 47: hyapp.luckygift.v1.ListLuckyGiftUserProfilesRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 46, // 48: hyapp.luckygift.v1.ListLuckyGiftUserProfilesResponse.profiles:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfile - 0, // 49: hyapp.luckygift.v1.GetLuckyGiftUserProfileRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 51, // 50: hyapp.luckygift.v1.LuckyGiftUserJackpotHit.conditions:type_name -> hyapp.luckygift.v1.LuckyGiftUserJackpotCondition - 46, // 51: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse.profile:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfile - 50, // 52: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse.multiplier_distribution:type_name -> hyapp.luckygift.v1.LuckyGiftUserMultiplierStat - 52, // 53: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse.jackpot_hits:type_name -> hyapp.luckygift.v1.LuckyGiftUserJackpotHit - 0, // 54: hyapp.luckygift.v1.ListLuckyGiftDailyStatsRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta - 55, // 55: hyapp.luckygift.v1.ListLuckyGiftDailyStatsResponse.stats:type_name -> hyapp.luckygift.v1.LuckyGiftDailyStat - 5, // 56: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:input_type -> hyapp.luckygift.v1.CheckLuckyGiftRequest - 8, // 57: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest - 10, // 58: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest - 12, // 59: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:input_type -> hyapp.luckygift.v1.ExternalGiftDrawRequest - 14, // 60: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:input_type -> hyapp.luckygift.v1.GetLuckyGiftConfigRequest - 16, // 61: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:input_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigRequest - 18, // 62: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:input_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsRequest - 20, // 63: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:input_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsRequest - 24, // 64: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:input_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest - 26, // 65: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:input_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest - 42, // 66: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:input_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest - 31, // 67: hyapp.luckygift.v1.AdminLuckyGiftService.CreateLuckyGiftExperiment:input_type -> hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest - 33, // 68: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftExperiments:input_type -> hyapp.luckygift.v1.ListLuckyGiftExperimentsRequest - 35, // 69: hyapp.luckygift.v1.AdminLuckyGiftService.UpdateLuckyGiftExperiment:input_type -> hyapp.luckygift.v1.UpdateLuckyGiftExperimentRequest - 37, // 70: hyapp.luckygift.v1.AdminLuckyGiftService.SetLuckyGiftExperimentOverrides:input_type -> hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesRequest - 39, // 71: hyapp.luckygift.v1.AdminLuckyGiftService.EndLuckyGiftExperiment:input_type -> hyapp.luckygift.v1.EndLuckyGiftExperimentRequest - 47, // 72: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftUserProfiles:input_type -> hyapp.luckygift.v1.ListLuckyGiftUserProfilesRequest - 49, // 73: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftUserProfile:input_type -> hyapp.luckygift.v1.GetLuckyGiftUserProfileRequest - 54, // 74: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDailyStats:input_type -> hyapp.luckygift.v1.ListLuckyGiftDailyStatsRequest - 6, // 75: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:output_type -> hyapp.luckygift.v1.CheckLuckyGiftResponse - 9, // 76: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse - 11, // 77: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawResponse - 13, // 78: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:output_type -> hyapp.luckygift.v1.ExternalGiftDrawResponse - 15, // 79: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:output_type -> hyapp.luckygift.v1.GetLuckyGiftConfigResponse - 17, // 80: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:output_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse - 19, // 81: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:output_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsResponse - 21, // 82: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:output_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsResponse - 25, // 83: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:output_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse - 27, // 84: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:output_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse - 44, // 85: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:output_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse - 32, // 86: hyapp.luckygift.v1.AdminLuckyGiftService.CreateLuckyGiftExperiment:output_type -> hyapp.luckygift.v1.CreateLuckyGiftExperimentResponse - 34, // 87: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftExperiments:output_type -> hyapp.luckygift.v1.ListLuckyGiftExperimentsResponse - 36, // 88: hyapp.luckygift.v1.AdminLuckyGiftService.UpdateLuckyGiftExperiment:output_type -> hyapp.luckygift.v1.UpdateLuckyGiftExperimentResponse - 38, // 89: hyapp.luckygift.v1.AdminLuckyGiftService.SetLuckyGiftExperimentOverrides:output_type -> hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesResponse - 40, // 90: hyapp.luckygift.v1.AdminLuckyGiftService.EndLuckyGiftExperiment:output_type -> hyapp.luckygift.v1.EndLuckyGiftExperimentResponse - 48, // 91: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftUserProfiles:output_type -> hyapp.luckygift.v1.ListLuckyGiftUserProfilesResponse - 53, // 92: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftUserProfile:output_type -> hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse - 56, // 93: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDailyStats:output_type -> hyapp.luckygift.v1.ListLuckyGiftDailyStatsResponse - 75, // [75:94] is the sub-list for method output_type - 56, // [56:75] is the sub-list for method input_type - 56, // [56:56] is the sub-list for extension type_name - 56, // [56:56] is the sub-list for extension extendee - 0, // [0:56] is the sub-list for field type_name + 0, // 17: hyapp.luckygift.v1.ListLuckyGiftConfigVersionsRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 4, // 18: hyapp.luckygift.v1.ListLuckyGiftConfigVersionsResponse.configs:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig + 0, // 19: hyapp.luckygift.v1.RollbackLuckyGiftConfigRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 4, // 20: hyapp.luckygift.v1.RollbackLuckyGiftConfigResponse.config:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig + 0, // 21: hyapp.luckygift.v1.ListLuckyGiftDrawsRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 7, // 22: hyapp.luckygift.v1.ListLuckyGiftDrawsResponse.draws:type_name -> hyapp.luckygift.v1.LuckyGiftDrawResult + 0, // 23: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 26, // 24: hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse.summary:type_name -> hyapp.luckygift.v1.LuckyGiftDrawSummary + 0, // 25: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 27, // 26: hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse.pools:type_name -> hyapp.luckygift.v1.LuckyGiftPoolBalance + 32, // 27: hyapp.luckygift.v1.LuckyGiftExperiment.overrides:type_name -> hyapp.luckygift.v1.LuckyGiftExperimentOverride + 33, // 28: hyapp.luckygift.v1.LuckyGiftExperiment.arm_stats:type_name -> hyapp.luckygift.v1.LuckyGiftExperimentArmStats + 0, // 29: hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 4, // 30: hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest.treatment_config:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig + 32, // 31: hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest.overrides:type_name -> hyapp.luckygift.v1.LuckyGiftExperimentOverride + 34, // 32: hyapp.luckygift.v1.CreateLuckyGiftExperimentResponse.experiment:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment + 4, // 33: hyapp.luckygift.v1.CreateLuckyGiftExperimentResponse.treatment_config:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig + 0, // 34: hyapp.luckygift.v1.ListLuckyGiftExperimentsRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 34, // 35: hyapp.luckygift.v1.ListLuckyGiftExperimentsResponse.experiments:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment + 0, // 36: hyapp.luckygift.v1.UpdateLuckyGiftExperimentRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 34, // 37: hyapp.luckygift.v1.UpdateLuckyGiftExperimentResponse.experiment:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment + 0, // 38: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 32, // 39: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesRequest.upserts:type_name -> hyapp.luckygift.v1.LuckyGiftExperimentOverride + 34, // 40: hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesResponse.experiment:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment + 0, // 41: hyapp.luckygift.v1.EndLuckyGiftExperimentRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 34, // 42: hyapp.luckygift.v1.EndLuckyGiftExperimentResponse.experiment:type_name -> hyapp.luckygift.v1.LuckyGiftExperiment + 4, // 43: hyapp.luckygift.v1.EndLuckyGiftExperimentResponse.final_config:type_name -> hyapp.luckygift.v1.LuckyGiftRuleConfig + 0, // 44: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 47, // 45: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse.adjustment:type_name -> hyapp.luckygift.v1.LuckyGiftPoolAdjustment + 27, // 46: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse.pool:type_name -> hyapp.luckygift.v1.LuckyGiftPoolBalance + 49, // 47: hyapp.luckygift.v1.LuckyGiftUserProfile.one_hour:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow + 49, // 48: hyapp.luckygift.v1.LuckyGiftUserProfile.twenty_four_hours:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow + 49, // 49: hyapp.luckygift.v1.LuckyGiftUserProfile.forty_eight_hours:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow + 49, // 50: hyapp.luckygift.v1.LuckyGiftUserProfile.lifetime:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow + 0, // 51: hyapp.luckygift.v1.ListLuckyGiftUserProfilesRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 50, // 52: hyapp.luckygift.v1.ListLuckyGiftUserProfilesResponse.profiles:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfile + 0, // 53: hyapp.luckygift.v1.GetLuckyGiftUserProfileRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 55, // 54: hyapp.luckygift.v1.LuckyGiftUserJackpotHit.conditions:type_name -> hyapp.luckygift.v1.LuckyGiftUserJackpotCondition + 50, // 55: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse.profile:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfile + 54, // 56: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse.multiplier_distribution:type_name -> hyapp.luckygift.v1.LuckyGiftUserMultiplierStat + 56, // 57: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse.jackpot_hits:type_name -> hyapp.luckygift.v1.LuckyGiftUserJackpotHit + 0, // 58: hyapp.luckygift.v1.ListLuckyGiftDailyStatsRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 59, // 59: hyapp.luckygift.v1.ListLuckyGiftDailyStatsResponse.stats:type_name -> hyapp.luckygift.v1.LuckyGiftDailyStat + 5, // 60: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:input_type -> hyapp.luckygift.v1.CheckLuckyGiftRequest + 8, // 61: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest + 10, // 62: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest + 12, // 63: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:input_type -> hyapp.luckygift.v1.ExternalGiftDrawRequest + 14, // 64: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:input_type -> hyapp.luckygift.v1.GetLuckyGiftConfigRequest + 16, // 65: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:input_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigRequest + 18, // 66: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:input_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsRequest + 20, // 67: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigVersions:input_type -> hyapp.luckygift.v1.ListLuckyGiftConfigVersionsRequest + 22, // 68: hyapp.luckygift.v1.AdminLuckyGiftService.RollbackLuckyGiftConfig:input_type -> hyapp.luckygift.v1.RollbackLuckyGiftConfigRequest + 24, // 69: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:input_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsRequest + 28, // 70: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:input_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest + 30, // 71: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:input_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest + 46, // 72: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:input_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest + 35, // 73: hyapp.luckygift.v1.AdminLuckyGiftService.CreateLuckyGiftExperiment:input_type -> hyapp.luckygift.v1.CreateLuckyGiftExperimentRequest + 37, // 74: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftExperiments:input_type -> hyapp.luckygift.v1.ListLuckyGiftExperimentsRequest + 39, // 75: hyapp.luckygift.v1.AdminLuckyGiftService.UpdateLuckyGiftExperiment:input_type -> hyapp.luckygift.v1.UpdateLuckyGiftExperimentRequest + 41, // 76: hyapp.luckygift.v1.AdminLuckyGiftService.SetLuckyGiftExperimentOverrides:input_type -> hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesRequest + 43, // 77: hyapp.luckygift.v1.AdminLuckyGiftService.EndLuckyGiftExperiment:input_type -> hyapp.luckygift.v1.EndLuckyGiftExperimentRequest + 51, // 78: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftUserProfiles:input_type -> hyapp.luckygift.v1.ListLuckyGiftUserProfilesRequest + 53, // 79: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftUserProfile:input_type -> hyapp.luckygift.v1.GetLuckyGiftUserProfileRequest + 58, // 80: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDailyStats:input_type -> hyapp.luckygift.v1.ListLuckyGiftDailyStatsRequest + 6, // 81: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:output_type -> hyapp.luckygift.v1.CheckLuckyGiftResponse + 9, // 82: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse + 11, // 83: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawResponse + 13, // 84: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:output_type -> hyapp.luckygift.v1.ExternalGiftDrawResponse + 15, // 85: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:output_type -> hyapp.luckygift.v1.GetLuckyGiftConfigResponse + 17, // 86: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:output_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse + 19, // 87: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:output_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsResponse + 21, // 88: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigVersions:output_type -> hyapp.luckygift.v1.ListLuckyGiftConfigVersionsResponse + 23, // 89: hyapp.luckygift.v1.AdminLuckyGiftService.RollbackLuckyGiftConfig:output_type -> hyapp.luckygift.v1.RollbackLuckyGiftConfigResponse + 25, // 90: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:output_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsResponse + 29, // 91: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:output_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse + 31, // 92: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:output_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse + 48, // 93: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:output_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse + 36, // 94: hyapp.luckygift.v1.AdminLuckyGiftService.CreateLuckyGiftExperiment:output_type -> hyapp.luckygift.v1.CreateLuckyGiftExperimentResponse + 38, // 95: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftExperiments:output_type -> hyapp.luckygift.v1.ListLuckyGiftExperimentsResponse + 40, // 96: hyapp.luckygift.v1.AdminLuckyGiftService.UpdateLuckyGiftExperiment:output_type -> hyapp.luckygift.v1.UpdateLuckyGiftExperimentResponse + 42, // 97: hyapp.luckygift.v1.AdminLuckyGiftService.SetLuckyGiftExperimentOverrides:output_type -> hyapp.luckygift.v1.SetLuckyGiftExperimentOverridesResponse + 44, // 98: hyapp.luckygift.v1.AdminLuckyGiftService.EndLuckyGiftExperiment:output_type -> hyapp.luckygift.v1.EndLuckyGiftExperimentResponse + 52, // 99: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftUserProfiles:output_type -> hyapp.luckygift.v1.ListLuckyGiftUserProfilesResponse + 57, // 100: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftUserProfile:output_type -> hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse + 60, // 101: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDailyStats:output_type -> hyapp.luckygift.v1.ListLuckyGiftDailyStatsResponse + 81, // [81:102] is the sub-list for method output_type + 60, // [60:81] is the sub-list for method input_type + 60, // [60:60] is the sub-list for extension type_name + 60, // [60:60] is the sub-list for extension extendee + 0, // [0:60] is the sub-list for field type_name } func init() { file_proto_luckygift_v1_luckygift_proto_init() } @@ -6123,14 +6368,14 @@ func file_proto_luckygift_v1_luckygift_proto_init() { if File_proto_luckygift_v1_luckygift_proto != nil { return } - file_proto_luckygift_v1_luckygift_proto_msgTypes[35].OneofWrappers = []any{} + file_proto_luckygift_v1_luckygift_proto_msgTypes[39].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_luckygift_v1_luckygift_proto_rawDesc), len(file_proto_luckygift_v1_luckygift_proto_rawDesc)), NumEnums: 0, - NumMessages: 57, + NumMessages: 61, NumExtensions: 0, NumServices: 2, }, diff --git a/api/proto/luckygift/v1/luckygift.proto b/api/proto/luckygift/v1/luckygift.proto index f913fdd8..35143ce9 100644 --- a/api/proto/luckygift/v1/luckygift.proto +++ b/api/proto/luckygift/v1/luckygift.proto @@ -247,6 +247,29 @@ message ListLuckyGiftConfigsResponse { repeated LuckyGiftRuleConfig configs = 1; } +// ListLuckyGiftConfigVersions 只读取同一 app+pool 的不可变历史快照;返回顺序为版本号倒序。 +message ListLuckyGiftConfigVersionsRequest { + RequestMeta meta = 1; + string pool_id = 2; +} + +message ListLuckyGiftConfigVersionsResponse { + repeated LuckyGiftRuleConfig configs = 1; +} + +// RollbackLuckyGiftConfig 不覆盖历史行,而是把目标快照复制成一个新的最新版本并立即生效。 +message RollbackLuckyGiftConfigRequest { + RequestMeta meta = 1; + string pool_id = 2; + int64 target_rule_version = 3; + int64 operator_admin_id = 4; +} + +message RollbackLuckyGiftConfigResponse { + int64 source_rule_version = 1; + LuckyGiftRuleConfig config = 2; +} + message ListLuckyGiftDrawsRequest { RequestMeta meta = 1; string gift_id = 2; @@ -444,6 +467,8 @@ service AdminLuckyGiftService { rpc GetLuckyGiftConfig(GetLuckyGiftConfigRequest) returns (GetLuckyGiftConfigResponse); rpc UpsertLuckyGiftConfig(UpsertLuckyGiftConfigRequest) returns (UpsertLuckyGiftConfigResponse); rpc ListLuckyGiftConfigs(ListLuckyGiftConfigsRequest) returns (ListLuckyGiftConfigsResponse); + rpc ListLuckyGiftConfigVersions(ListLuckyGiftConfigVersionsRequest) returns (ListLuckyGiftConfigVersionsResponse); + rpc RollbackLuckyGiftConfig(RollbackLuckyGiftConfigRequest) returns (RollbackLuckyGiftConfigResponse); rpc ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse); rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse); rpc ListLuckyGiftPoolBalances(ListLuckyGiftPoolBalancesRequest) returns (ListLuckyGiftPoolBalancesResponse); diff --git a/api/proto/luckygift/v1/luckygift_grpc.pb.go b/api/proto/luckygift/v1/luckygift_grpc.pb.go index 9448619c..53de0604 100644 --- a/api/proto/luckygift/v1/luckygift_grpc.pb.go +++ b/api/proto/luckygift/v1/luckygift_grpc.pb.go @@ -238,6 +238,8 @@ const ( AdminLuckyGiftService_GetLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftConfig" AdminLuckyGiftService_UpsertLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/UpsertLuckyGiftConfig" AdminLuckyGiftService_ListLuckyGiftConfigs_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftConfigs" + AdminLuckyGiftService_ListLuckyGiftConfigVersions_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftConfigVersions" + AdminLuckyGiftService_RollbackLuckyGiftConfig_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/RollbackLuckyGiftConfig" AdminLuckyGiftService_ListLuckyGiftDraws_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftDraws" AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary" AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftPoolBalances" @@ -259,6 +261,8 @@ type AdminLuckyGiftServiceClient interface { GetLuckyGiftConfig(ctx context.Context, in *GetLuckyGiftConfigRequest, opts ...grpc.CallOption) (*GetLuckyGiftConfigResponse, error) UpsertLuckyGiftConfig(ctx context.Context, in *UpsertLuckyGiftConfigRequest, opts ...grpc.CallOption) (*UpsertLuckyGiftConfigResponse, error) ListLuckyGiftConfigs(ctx context.Context, in *ListLuckyGiftConfigsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigsResponse, error) + ListLuckyGiftConfigVersions(ctx context.Context, in *ListLuckyGiftConfigVersionsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigVersionsResponse, error) + RollbackLuckyGiftConfig(ctx context.Context, in *RollbackLuckyGiftConfigRequest, opts ...grpc.CallOption) (*RollbackLuckyGiftConfigResponse, error) ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error) GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error) ListLuckyGiftPoolBalances(ctx context.Context, in *ListLuckyGiftPoolBalancesRequest, opts ...grpc.CallOption) (*ListLuckyGiftPoolBalancesResponse, error) @@ -311,6 +315,26 @@ func (c *adminLuckyGiftServiceClient) ListLuckyGiftConfigs(ctx context.Context, return out, nil } +func (c *adminLuckyGiftServiceClient) ListLuckyGiftConfigVersions(ctx context.Context, in *ListLuckyGiftConfigVersionsRequest, opts ...grpc.CallOption) (*ListLuckyGiftConfigVersionsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListLuckyGiftConfigVersionsResponse) + err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftConfigVersions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *adminLuckyGiftServiceClient) RollbackLuckyGiftConfig(ctx context.Context, in *RollbackLuckyGiftConfigRequest, opts ...grpc.CallOption) (*RollbackLuckyGiftConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RollbackLuckyGiftConfigResponse) + err := c.cc.Invoke(ctx, AdminLuckyGiftService_RollbackLuckyGiftConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *adminLuckyGiftServiceClient) ListLuckyGiftDraws(ctx context.Context, in *ListLuckyGiftDrawsRequest, opts ...grpc.CallOption) (*ListLuckyGiftDrawsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListLuckyGiftDrawsResponse) @@ -438,6 +462,8 @@ type AdminLuckyGiftServiceServer interface { GetLuckyGiftConfig(context.Context, *GetLuckyGiftConfigRequest) (*GetLuckyGiftConfigResponse, error) UpsertLuckyGiftConfig(context.Context, *UpsertLuckyGiftConfigRequest) (*UpsertLuckyGiftConfigResponse, error) ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error) + ListLuckyGiftConfigVersions(context.Context, *ListLuckyGiftConfigVersionsRequest) (*ListLuckyGiftConfigVersionsResponse, error) + RollbackLuckyGiftConfig(context.Context, *RollbackLuckyGiftConfigRequest) (*RollbackLuckyGiftConfigResponse, error) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error) ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error) @@ -469,6 +495,12 @@ func (UnimplementedAdminLuckyGiftServiceServer) UpsertLuckyGiftConfig(context.Co func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigs(context.Context, *ListLuckyGiftConfigsRequest) (*ListLuckyGiftConfigsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftConfigs not implemented") } +func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftConfigVersions(context.Context, *ListLuckyGiftConfigVersionsRequest) (*ListLuckyGiftConfigVersionsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftConfigVersions not implemented") +} +func (UnimplementedAdminLuckyGiftServiceServer) RollbackLuckyGiftConfig(context.Context, *RollbackLuckyGiftConfigRequest) (*RollbackLuckyGiftConfigResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RollbackLuckyGiftConfig not implemented") +} func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftDraws(context.Context, *ListLuckyGiftDrawsRequest) (*ListLuckyGiftDrawsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftDraws not implemented") } @@ -580,6 +612,42 @@ func _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler(srv interface{}, ctx co return interceptor(ctx, in, info, handler) } +func _AdminLuckyGiftService_ListLuckyGiftConfigVersions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListLuckyGiftConfigVersionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigVersions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminLuckyGiftService_ListLuckyGiftConfigVersions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftConfigVersions(ctx, req.(*ListLuckyGiftConfigVersionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AdminLuckyGiftService_RollbackLuckyGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RollbackLuckyGiftConfigRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminLuckyGiftServiceServer).RollbackLuckyGiftConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminLuckyGiftService_RollbackLuckyGiftConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminLuckyGiftServiceServer).RollbackLuckyGiftConfig(ctx, req.(*RollbackLuckyGiftConfigRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _AdminLuckyGiftService_ListLuckyGiftDraws_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListLuckyGiftDrawsRequest) if err := dec(in); err != nil { @@ -815,6 +883,14 @@ var AdminLuckyGiftService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListLuckyGiftConfigs", Handler: _AdminLuckyGiftService_ListLuckyGiftConfigs_Handler, }, + { + MethodName: "ListLuckyGiftConfigVersions", + Handler: _AdminLuckyGiftService_ListLuckyGiftConfigVersions_Handler, + }, + { + MethodName: "RollbackLuckyGiftConfig", + Handler: _AdminLuckyGiftService_RollbackLuckyGiftConfig_Handler, + }, { MethodName: "ListLuckyGiftDraws", Handler: _AdminLuckyGiftService_ListLuckyGiftDraws_Handler, diff --git a/api/proto/room/v1/room.pb.go b/api/proto/room/v1/room.pb.go index 628a8aa9..02a8d6c7 100644 --- a/api/proto/room/v1/room.pb.go +++ b/api/proto/room/v1/room.pb.go @@ -4584,9 +4584,14 @@ type RoomSnapshot struct { // ban_states 保存 ban 的过期边界;ban_user_ids 仍只服务简单集合判断。 BanStates []*RoomBanState `protobuf:"bytes,21,rep,name=ban_states,json=banStates,proto3" json:"ban_states,omitempty"` // online_count 允许 lite 响应不携带 online_users 明细仍保留准确在线人数。 - OnlineCount int32 `protobuf:"varint,22,opt,name=online_count,json=onlineCount,proto3" json:"online_count,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + OnlineCount int32 `protobuf:"varint,22,opt,name=online_count,json=onlineCount,proto3" json:"online_count,omitempty"` + // room_border 与 room_name_style 是房间级最终装扮快照;客户端不得再按房主 VIP 等级推导样式。 + RoomBorder *RoomDecorationResource `protobuf:"bytes,23,opt,name=room_border,json=roomBorder,proto3" json:"room_border,omitempty"` + RoomNameStyle *RoomDecorationResource `protobuf:"bytes,24,opt,name=room_name_style,json=roomNameStyle,proto3" json:"room_name_style,omitempty"` + // active_background 固化当前背景的真实媒体属性;room_ext.background_url 仅保留旧客户端兼容。 + ActiveBackground *RoomBackgroundImage `protobuf:"bytes,25,opt,name=active_background,json=activeBackground,proto3" json:"active_background,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RoomSnapshot) Reset() { @@ -4766,6 +4771,177 @@ func (x *RoomSnapshot) GetOnlineCount() int32 { return 0 } +func (x *RoomSnapshot) GetRoomBorder() *RoomDecorationResource { + if x != nil { + return x.RoomBorder + } + return nil +} + +func (x *RoomSnapshot) GetRoomNameStyle() *RoomDecorationResource { + if x != nil { + return x.RoomNameStyle + } + return nil +} + +func (x *RoomSnapshot) GetActiveBackground() *RoomBackgroundImage { + if x != nil { + return x.ActiveBackground + } + return nil +} + +// RoomMedia 是房间背景和房间图片消息共同使用的可信媒体快照。 +// URL 只能由 gateway 的受信对象存储上传入口生成;room-service 会再次校验 object_key 归属和媒体边界。 +type RoomMedia struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + ObjectKey string `protobuf:"bytes,2,opt,name=object_key,json=objectKey,proto3" json:"object_key,omitempty"` + ContentType string `protobuf:"bytes,3,opt,name=content_type,json=contentType,proto3" json:"content_type,omitempty"` + Format string `protobuf:"bytes,4,opt,name=format,proto3" json:"format,omitempty"` + SizeBytes int64 `protobuf:"varint,5,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + Width int32 `protobuf:"varint,6,opt,name=width,proto3" json:"width,omitempty"` + Height int32 `protobuf:"varint,7,opt,name=height,proto3" json:"height,omitempty"` + Animated bool `protobuf:"varint,8,opt,name=animated,proto3" json:"animated,omitempty"` + FrameCount int32 `protobuf:"varint,9,opt,name=frame_count,json=frameCount,proto3" json:"frame_count,omitempty"` + DurationMs int64 `protobuf:"varint,10,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + PosterUrl string `protobuf:"bytes,11,opt,name=poster_url,json=posterUrl,proto3" json:"poster_url,omitempty"` + ThumbnailUrl string `protobuf:"bytes,12,opt,name=thumbnail_url,json=thumbnailUrl,proto3" json:"thumbnail_url,omitempty"` + Sha256 string `protobuf:"bytes,13,opt,name=sha256,proto3" json:"sha256,omitempty"` + Status string `protobuf:"bytes,14,opt,name=status,proto3" json:"status,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoomMedia) Reset() { + *x = RoomMedia{} + mi := &file_proto_room_v1_room_proto_msgTypes[55] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoomMedia) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoomMedia) ProtoMessage() {} + +func (x *RoomMedia) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[55] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoomMedia.ProtoReflect.Descriptor instead. +func (*RoomMedia) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{55} +} + +func (x *RoomMedia) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *RoomMedia) GetObjectKey() string { + if x != nil { + return x.ObjectKey + } + return "" +} + +func (x *RoomMedia) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + +func (x *RoomMedia) GetFormat() string { + if x != nil { + return x.Format + } + return "" +} + +func (x *RoomMedia) GetSizeBytes() int64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +func (x *RoomMedia) GetWidth() int32 { + if x != nil { + return x.Width + } + return 0 +} + +func (x *RoomMedia) GetHeight() int32 { + if x != nil { + return x.Height + } + return 0 +} + +func (x *RoomMedia) GetAnimated() bool { + if x != nil { + return x.Animated + } + return false +} + +func (x *RoomMedia) GetFrameCount() int32 { + if x != nil { + return x.FrameCount + } + return 0 +} + +func (x *RoomMedia) GetDurationMs() int64 { + if x != nil { + return x.DurationMs + } + return 0 +} + +func (x *RoomMedia) GetPosterUrl() string { + if x != nil { + return x.PosterUrl + } + return "" +} + +func (x *RoomMedia) GetThumbnailUrl() string { + if x != nil { + return x.ThumbnailUrl + } + return "" +} + +func (x *RoomMedia) GetSha256() string { + if x != nil { + return x.Sha256 + } + return "" +} + +func (x *RoomMedia) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + // RoomBackgroundImage 是房间维度保存过的背景图素材。 // 保存列表归属于 room-service,当前生效背景仍写入 RoomSnapshot.room_ext["background_url"]。 type RoomBackgroundImage struct { @@ -4776,13 +4952,15 @@ type RoomBackgroundImage struct { CreatedByUserId int64 `protobuf:"varint,4,opt,name=created_by_user_id,json=createdByUserId,proto3" json:"created_by_user_id,omitempty"` CreatedAtMs int64 `protobuf:"varint,5,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` UpdatedAtMs int64 `protobuf:"varint,6,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` + Media *RoomMedia `protobuf:"bytes,7,opt,name=media,proto3" json:"media,omitempty"` + CommandId string `protobuf:"bytes,8,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RoomBackgroundImage) Reset() { *x = RoomBackgroundImage{} - mi := &file_proto_room_v1_room_proto_msgTypes[55] + mi := &file_proto_room_v1_room_proto_msgTypes[56] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4794,7 +4972,7 @@ func (x *RoomBackgroundImage) String() string { func (*RoomBackgroundImage) ProtoMessage() {} func (x *RoomBackgroundImage) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[55] + mi := &file_proto_room_v1_room_proto_msgTypes[56] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4807,7 +4985,7 @@ func (x *RoomBackgroundImage) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomBackgroundImage.ProtoReflect.Descriptor instead. func (*RoomBackgroundImage) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{55} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{56} } func (x *RoomBackgroundImage) GetBackgroundId() int64 { @@ -4852,18 +5030,34 @@ func (x *RoomBackgroundImage) GetUpdatedAtMs() int64 { return 0 } +func (x *RoomBackgroundImage) GetMedia() *RoomMedia { + if x != nil { + return x.Media + } + return nil +} + +func (x *RoomBackgroundImage) GetCommandId() string { + if x != nil { + return x.CommandId + } + return "" +} + type SaveRoomBackgroundRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` - RoomId string `protobuf:"bytes,2,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"` - ImageUrl string `protobuf:"bytes,3,opt,name=image_url,json=imageUrl,proto3" json:"image_url,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + RoomId string `protobuf:"bytes,2,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"` + // image_url 仅兼容旧内部调用;新版专用上传必须传完整 media,保存时不按 URL 后缀猜格式。 + ImageUrl string `protobuf:"bytes,3,opt,name=image_url,json=imageUrl,proto3" json:"image_url,omitempty"` + Media *RoomMedia `protobuf:"bytes,4,opt,name=media,proto3" json:"media,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *SaveRoomBackgroundRequest) Reset() { *x = SaveRoomBackgroundRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[56] + mi := &file_proto_room_v1_room_proto_msgTypes[57] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4875,7 +5069,7 @@ func (x *SaveRoomBackgroundRequest) String() string { func (*SaveRoomBackgroundRequest) ProtoMessage() {} func (x *SaveRoomBackgroundRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[56] + mi := &file_proto_room_v1_room_proto_msgTypes[57] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4888,7 +5082,7 @@ func (x *SaveRoomBackgroundRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveRoomBackgroundRequest.ProtoReflect.Descriptor instead. func (*SaveRoomBackgroundRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{56} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{57} } func (x *SaveRoomBackgroundRequest) GetMeta() *RequestMeta { @@ -4912,6 +5106,13 @@ func (x *SaveRoomBackgroundRequest) GetImageUrl() string { return "" } +func (x *SaveRoomBackgroundRequest) GetMedia() *RoomMedia { + if x != nil { + return x.Media + } + return nil +} + type SaveRoomBackgroundResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Background *RoomBackgroundImage `protobuf:"bytes,1,opt,name=background,proto3" json:"background,omitempty"` @@ -4922,7 +5123,7 @@ type SaveRoomBackgroundResponse struct { func (x *SaveRoomBackgroundResponse) Reset() { *x = SaveRoomBackgroundResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[57] + mi := &file_proto_room_v1_room_proto_msgTypes[58] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4934,7 +5135,7 @@ func (x *SaveRoomBackgroundResponse) String() string { func (*SaveRoomBackgroundResponse) ProtoMessage() {} func (x *SaveRoomBackgroundResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[57] + mi := &file_proto_room_v1_room_proto_msgTypes[58] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4947,7 +5148,7 @@ func (x *SaveRoomBackgroundResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SaveRoomBackgroundResponse.ProtoReflect.Descriptor instead. func (*SaveRoomBackgroundResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{57} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{58} } func (x *SaveRoomBackgroundResponse) GetBackground() *RoomBackgroundImage { @@ -4975,7 +5176,7 @@ type ListRoomBackgroundsRequest struct { func (x *ListRoomBackgroundsRequest) Reset() { *x = ListRoomBackgroundsRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[58] + mi := &file_proto_room_v1_room_proto_msgTypes[59] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4987,7 +5188,7 @@ func (x *ListRoomBackgroundsRequest) String() string { func (*ListRoomBackgroundsRequest) ProtoMessage() {} func (x *ListRoomBackgroundsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[58] + mi := &file_proto_room_v1_room_proto_msgTypes[59] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5000,7 +5201,7 @@ func (x *ListRoomBackgroundsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomBackgroundsRequest.ProtoReflect.Descriptor instead. func (*ListRoomBackgroundsRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{58} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{59} } func (x *ListRoomBackgroundsRequest) GetMeta() *RequestMeta { @@ -5029,13 +5230,14 @@ type ListRoomBackgroundsResponse struct { Backgrounds []*RoomBackgroundImage `protobuf:"bytes,1,rep,name=backgrounds,proto3" json:"backgrounds,omitempty"` SelectedBackgroundUrl string `protobuf:"bytes,2,opt,name=selected_background_url,json=selectedBackgroundUrl,proto3" json:"selected_background_url,omitempty"` ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + SelectedBackground *RoomBackgroundImage `protobuf:"bytes,4,opt,name=selected_background,json=selectedBackground,proto3" json:"selected_background,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ListRoomBackgroundsResponse) Reset() { *x = ListRoomBackgroundsResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[59] + mi := &file_proto_room_v1_room_proto_msgTypes[60] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5047,7 +5249,7 @@ func (x *ListRoomBackgroundsResponse) String() string { func (*ListRoomBackgroundsResponse) ProtoMessage() {} func (x *ListRoomBackgroundsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[59] + mi := &file_proto_room_v1_room_proto_msgTypes[60] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5060,7 +5262,7 @@ func (x *ListRoomBackgroundsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomBackgroundsResponse.ProtoReflect.Descriptor instead. func (*ListRoomBackgroundsResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{59} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{60} } func (x *ListRoomBackgroundsResponse) GetBackgrounds() []*RoomBackgroundImage { @@ -5084,6 +5286,13 @@ func (x *ListRoomBackgroundsResponse) GetServerTimeMs() int64 { return 0 } +func (x *ListRoomBackgroundsResponse) GetSelectedBackground() *RoomBackgroundImage { + if x != nil { + return x.SelectedBackground + } + return nil +} + type SetRoomBackgroundRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` @@ -5094,7 +5303,7 @@ type SetRoomBackgroundRequest struct { func (x *SetRoomBackgroundRequest) Reset() { *x = SetRoomBackgroundRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[60] + mi := &file_proto_room_v1_room_proto_msgTypes[61] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5106,7 +5315,7 @@ func (x *SetRoomBackgroundRequest) String() string { func (*SetRoomBackgroundRequest) ProtoMessage() {} func (x *SetRoomBackgroundRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[60] + mi := &file_proto_room_v1_room_proto_msgTypes[61] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5119,7 +5328,7 @@ func (x *SetRoomBackgroundRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRoomBackgroundRequest.ProtoReflect.Descriptor instead. func (*SetRoomBackgroundRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{60} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{61} } func (x *SetRoomBackgroundRequest) GetMeta() *RequestMeta { @@ -5147,7 +5356,7 @@ type SetRoomBackgroundResponse struct { func (x *SetRoomBackgroundResponse) Reset() { *x = SetRoomBackgroundResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[61] + mi := &file_proto_room_v1_room_proto_msgTypes[62] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5159,7 +5368,7 @@ func (x *SetRoomBackgroundResponse) String() string { func (*SetRoomBackgroundResponse) ProtoMessage() {} func (x *SetRoomBackgroundResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[61] + mi := &file_proto_room_v1_room_proto_msgTypes[62] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5172,7 +5381,7 @@ func (x *SetRoomBackgroundResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRoomBackgroundResponse.ProtoReflect.Descriptor instead. func (*SetRoomBackgroundResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{61} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{62} } func (x *SetRoomBackgroundResponse) GetResult() *CommandResult { @@ -5196,6 +5405,704 @@ func (x *SetRoomBackgroundResponse) GetBackground() *RoomBackgroundImage { return nil } +// RoomDecorationResource 是 wallet 资源目录和有效 entitlement 在房间应用时固化的展示快照。 +// 资源后续编辑、下架或 VIP 到期不会篡改历史 command log;新的读取只返回当前 Room Cell 已应用结果。 +type RoomDecorationResource struct { + state protoimpl.MessageState `protogen:"open.v1"` + ResourceId int64 `protobuf:"varint,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` + ResourceCode string `protobuf:"bytes,2,opt,name=resource_code,json=resourceCode,proto3" json:"resource_code,omitempty"` + ResourceType string `protobuf:"bytes,3,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"` + Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"` + AssetUrl string `protobuf:"bytes,5,opt,name=asset_url,json=assetUrl,proto3" json:"asset_url,omitempty"` + PreviewUrl string `protobuf:"bytes,6,opt,name=preview_url,json=previewUrl,proto3" json:"preview_url,omitempty"` + AnimationUrl string `protobuf:"bytes,7,opt,name=animation_url,json=animationUrl,proto3" json:"animation_url,omitempty"` + Format string `protobuf:"bytes,8,opt,name=format,proto3" json:"format,omitempty"` + MetadataJson string `protobuf:"bytes,9,opt,name=metadata_json,json=metadataJson,proto3" json:"metadata_json,omitempty"` + EntitlementId string `protobuf:"bytes,10,opt,name=entitlement_id,json=entitlementId,proto3" json:"entitlement_id,omitempty"` + TextColors []string `protobuf:"bytes,11,rep,name=text_colors,json=textColors,proto3" json:"text_colors,omitempty"` + Stops []float64 `protobuf:"fixed64,12,rep,packed,name=stops,proto3" json:"stops,omitempty"` + AngleDegrees float64 `protobuf:"fixed64,13,opt,name=angle_degrees,json=angleDegrees,proto3" json:"angle_degrees,omitempty"` + ExpiresAtMs int64 `protobuf:"varint,14,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RoomDecorationResource) Reset() { + *x = RoomDecorationResource{} + mi := &file_proto_room_v1_room_proto_msgTypes[63] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RoomDecorationResource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RoomDecorationResource) ProtoMessage() {} + +func (x *RoomDecorationResource) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[63] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RoomDecorationResource.ProtoReflect.Descriptor instead. +func (*RoomDecorationResource) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{63} +} + +func (x *RoomDecorationResource) GetResourceId() int64 { + if x != nil { + return x.ResourceId + } + return 0 +} + +func (x *RoomDecorationResource) GetResourceCode() string { + if x != nil { + return x.ResourceCode + } + return "" +} + +func (x *RoomDecorationResource) GetResourceType() string { + if x != nil { + return x.ResourceType + } + return "" +} + +func (x *RoomDecorationResource) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *RoomDecorationResource) GetAssetUrl() string { + if x != nil { + return x.AssetUrl + } + return "" +} + +func (x *RoomDecorationResource) GetPreviewUrl() string { + if x != nil { + return x.PreviewUrl + } + return "" +} + +func (x *RoomDecorationResource) GetAnimationUrl() string { + if x != nil { + return x.AnimationUrl + } + return "" +} + +func (x *RoomDecorationResource) GetFormat() string { + if x != nil { + return x.Format + } + return "" +} + +func (x *RoomDecorationResource) GetMetadataJson() string { + if x != nil { + return x.MetadataJson + } + return "" +} + +func (x *RoomDecorationResource) GetEntitlementId() string { + if x != nil { + return x.EntitlementId + } + return "" +} + +func (x *RoomDecorationResource) GetTextColors() []string { + if x != nil { + return x.TextColors + } + return nil +} + +func (x *RoomDecorationResource) GetStops() []float64 { + if x != nil { + return x.Stops + } + return nil +} + +func (x *RoomDecorationResource) GetAngleDegrees() float64 { + if x != nil { + return x.AngleDegrees + } + return 0 +} + +func (x *RoomDecorationResource) GetExpiresAtMs() int64 { + if x != nil { + return x.ExpiresAtMs + } + return 0 +} + +type ApplyRoomDecorationRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + // decoration_type 仅接受 room_border/colored_room_name;资源类型分别固定为 room_border/room_name_style。 + DecorationType string `protobuf:"bytes,2,opt,name=decoration_type,json=decorationType,proto3" json:"decoration_type,omitempty"` + ResourceId int64 `protobuf:"varint,3,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` + EntitlementId string `protobuf:"bytes,4,opt,name=entitlement_id,json=entitlementId,proto3" json:"entitlement_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyRoomDecorationRequest) Reset() { + *x = ApplyRoomDecorationRequest{} + mi := &file_proto_room_v1_room_proto_msgTypes[64] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyRoomDecorationRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyRoomDecorationRequest) ProtoMessage() {} + +func (x *ApplyRoomDecorationRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[64] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyRoomDecorationRequest.ProtoReflect.Descriptor instead. +func (*ApplyRoomDecorationRequest) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{64} +} + +func (x *ApplyRoomDecorationRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *ApplyRoomDecorationRequest) GetDecorationType() string { + if x != nil { + return x.DecorationType + } + return "" +} + +func (x *ApplyRoomDecorationRequest) GetResourceId() int64 { + if x != nil { + return x.ResourceId + } + return 0 +} + +func (x *ApplyRoomDecorationRequest) GetEntitlementId() string { + if x != nil { + return x.EntitlementId + } + return "" +} + +type ApplyRoomDecorationResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result *CommandResult `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + Room *RoomSnapshot `protobuf:"bytes,2,opt,name=room,proto3" json:"room,omitempty"` + DecorationType string `protobuf:"bytes,3,opt,name=decoration_type,json=decorationType,proto3" json:"decoration_type,omitempty"` + Resource *RoomDecorationResource `protobuf:"bytes,4,opt,name=resource,proto3" json:"resource,omitempty"` + ServerTimeMs int64 `protobuf:"varint,5,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ApplyRoomDecorationResponse) Reset() { + *x = ApplyRoomDecorationResponse{} + mi := &file_proto_room_v1_room_proto_msgTypes[65] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ApplyRoomDecorationResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApplyRoomDecorationResponse) ProtoMessage() {} + +func (x *ApplyRoomDecorationResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[65] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApplyRoomDecorationResponse.ProtoReflect.Descriptor instead. +func (*ApplyRoomDecorationResponse) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{65} +} + +func (x *ApplyRoomDecorationResponse) GetResult() *CommandResult { + if x != nil { + return x.Result + } + return nil +} + +func (x *ApplyRoomDecorationResponse) GetRoom() *RoomSnapshot { + if x != nil { + return x.Room + } + return nil +} + +func (x *ApplyRoomDecorationResponse) GetDecorationType() string { + if x != nil { + return x.DecorationType + } + return "" +} + +func (x *ApplyRoomDecorationResponse) GetResource() *RoomDecorationResource { + if x != nil { + return x.Resource + } + return nil +} + +func (x *ApplyRoomDecorationResponse) GetServerTimeMs() int64 { + if x != nil { + return x.ServerTimeMs + } + return 0 +} + +type AuthorizeRoomMediaUploadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + // purpose 仅接受 room_background/room_image;授权发生在 COS PutObject 之前。 + Purpose string `protobuf:"bytes,2,opt,name=purpose,proto3" json:"purpose,omitempty"` + Media *RoomMedia `protobuf:"bytes,3,opt,name=media,proto3" json:"media,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizeRoomMediaUploadRequest) Reset() { + *x = AuthorizeRoomMediaUploadRequest{} + mi := &file_proto_room_v1_room_proto_msgTypes[66] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizeRoomMediaUploadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizeRoomMediaUploadRequest) ProtoMessage() {} + +func (x *AuthorizeRoomMediaUploadRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[66] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizeRoomMediaUploadRequest.ProtoReflect.Descriptor instead. +func (*AuthorizeRoomMediaUploadRequest) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{66} +} + +func (x *AuthorizeRoomMediaUploadRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *AuthorizeRoomMediaUploadRequest) GetPurpose() string { + if x != nil { + return x.Purpose + } + return "" +} + +func (x *AuthorizeRoomMediaUploadRequest) GetMedia() *RoomMedia { + if x != nil { + return x.Media + } + return nil +} + +type AuthorizeRoomMediaUploadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Allowed bool `protobuf:"varint,1,opt,name=allowed,proto3" json:"allowed,omitempty"` + ObjectKeyPrefix string `protobuf:"bytes,2,opt,name=object_key_prefix,json=objectKeyPrefix,proto3" json:"object_key_prefix,omitempty"` + RoomVersion int64 `protobuf:"varint,3,opt,name=room_version,json=roomVersion,proto3" json:"room_version,omitempty"` + ServerTimeMs int64 `protobuf:"varint,4,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + // object_key 由 room owner 按首次 command_id+媒体哈希确定,gateway 不能自行换 key。 + ObjectKey string `protobuf:"bytes,5,opt,name=object_key,json=objectKey,proto3" json:"object_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizeRoomMediaUploadResponse) Reset() { + *x = AuthorizeRoomMediaUploadResponse{} + mi := &file_proto_room_v1_room_proto_msgTypes[67] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizeRoomMediaUploadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizeRoomMediaUploadResponse) ProtoMessage() {} + +func (x *AuthorizeRoomMediaUploadResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[67] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizeRoomMediaUploadResponse.ProtoReflect.Descriptor instead. +func (*AuthorizeRoomMediaUploadResponse) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{67} +} + +func (x *AuthorizeRoomMediaUploadResponse) GetAllowed() bool { + if x != nil { + return x.Allowed + } + return false +} + +func (x *AuthorizeRoomMediaUploadResponse) GetObjectKeyPrefix() string { + if x != nil { + return x.ObjectKeyPrefix + } + return "" +} + +func (x *AuthorizeRoomMediaUploadResponse) GetRoomVersion() int64 { + if x != nil { + return x.RoomVersion + } + return 0 +} + +func (x *AuthorizeRoomMediaUploadResponse) GetServerTimeMs() int64 { + if x != nil { + return x.ServerTimeMs + } + return 0 +} + +func (x *AuthorizeRoomMediaUploadResponse) GetObjectKey() string { + if x != nil { + return x.ObjectKey + } + return "" +} + +type CompleteRoomMediaUploadRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + Purpose string `protobuf:"bytes,2,opt,name=purpose,proto3" json:"purpose,omitempty"` + // media 必须携带 PutObject 成功后的 url/object_key,并与首次 Authorize 的解析事实完全一致。 + Media *RoomMedia `protobuf:"bytes,3,opt,name=media,proto3" json:"media,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CompleteRoomMediaUploadRequest) Reset() { + *x = CompleteRoomMediaUploadRequest{} + mi := &file_proto_room_v1_room_proto_msgTypes[68] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CompleteRoomMediaUploadRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CompleteRoomMediaUploadRequest) ProtoMessage() {} + +func (x *CompleteRoomMediaUploadRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[68] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CompleteRoomMediaUploadRequest.ProtoReflect.Descriptor instead. +func (*CompleteRoomMediaUploadRequest) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{68} +} + +func (x *CompleteRoomMediaUploadRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *CompleteRoomMediaUploadRequest) GetPurpose() string { + if x != nil { + return x.Purpose + } + return "" +} + +func (x *CompleteRoomMediaUploadRequest) GetMedia() *RoomMedia { + if x != nil { + return x.Media + } + return nil +} + +type CompleteRoomMediaUploadResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Active bool `protobuf:"varint,1,opt,name=active,proto3" json:"active,omitempty"` + Media *RoomMedia `protobuf:"bytes,2,opt,name=media,proto3" json:"media,omitempty"` + ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CompleteRoomMediaUploadResponse) Reset() { + *x = CompleteRoomMediaUploadResponse{} + mi := &file_proto_room_v1_room_proto_msgTypes[69] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CompleteRoomMediaUploadResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CompleteRoomMediaUploadResponse) ProtoMessage() {} + +func (x *CompleteRoomMediaUploadResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[69] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CompleteRoomMediaUploadResponse.ProtoReflect.Descriptor instead. +func (*CompleteRoomMediaUploadResponse) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{69} +} + +func (x *CompleteRoomMediaUploadResponse) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + +func (x *CompleteRoomMediaUploadResponse) GetMedia() *RoomMedia { + if x != nil { + return x.Media + } + return nil +} + +func (x *CompleteRoomMediaUploadResponse) GetServerTimeMs() int64 { + if x != nil { + return x.ServerTimeMs + } + return 0 +} + +type SendRoomImageRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + Image *RoomMedia `protobuf:"bytes,2,opt,name=image,proto3" json:"image,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendRoomImageRequest) Reset() { + *x = SendRoomImageRequest{} + mi := &file_proto_room_v1_room_proto_msgTypes[70] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendRoomImageRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendRoomImageRequest) ProtoMessage() {} + +func (x *SendRoomImageRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[70] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendRoomImageRequest.ProtoReflect.Descriptor instead. +func (*SendRoomImageRequest) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{70} +} + +func (x *SendRoomImageRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *SendRoomImageRequest) GetImage() *RoomMedia { + if x != nil { + return x.Image + } + return nil +} + +type SendRoomImageResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Result *CommandResult `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"` + Accepted bool `protobuf:"varint,2,opt,name=accepted,proto3" json:"accepted,omitempty"` + MessageId string `protobuf:"bytes,3,opt,name=message_id,json=messageId,proto3" json:"message_id,omitempty"` + RoomId string `protobuf:"bytes,4,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"` + SenderUserId int64 `protobuf:"varint,5,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"` + ServerTimeMs int64 `protobuf:"varint,6,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + Image *RoomMedia `protobuf:"bytes,7,opt,name=image,proto3" json:"image,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SendRoomImageResponse) Reset() { + *x = SendRoomImageResponse{} + mi := &file_proto_room_v1_room_proto_msgTypes[71] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SendRoomImageResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SendRoomImageResponse) ProtoMessage() {} + +func (x *SendRoomImageResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[71] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SendRoomImageResponse.ProtoReflect.Descriptor instead. +func (*SendRoomImageResponse) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{71} +} + +func (x *SendRoomImageResponse) GetResult() *CommandResult { + if x != nil { + return x.Result + } + return nil +} + +func (x *SendRoomImageResponse) GetAccepted() bool { + if x != nil { + return x.Accepted + } + return false +} + +func (x *SendRoomImageResponse) GetMessageId() string { + if x != nil { + return x.MessageId + } + return "" +} + +func (x *SendRoomImageResponse) GetRoomId() string { + if x != nil { + return x.RoomId + } + return "" +} + +func (x *SendRoomImageResponse) GetSenderUserId() int64 { + if x != nil { + return x.SenderUserId + } + return 0 +} + +func (x *SendRoomImageResponse) GetServerTimeMs() int64 { + if x != nil { + return x.ServerTimeMs + } + return 0 +} + +func (x *SendRoomImageResponse) GetImage() *RoomMedia { + if x != nil { + return x.Image + } + return nil +} + // CreateRoomRequest 创建一个新的房间执行单元。 // 必填语义:meta.room_id、meta.command_id、meta.actor_user_id、mode、room_name 和 room_avatar。 // 同一个 meta.actor_user_id 只能作为 owner 创建一个房间;重复创建返回 Conflict。 @@ -5228,7 +6135,7 @@ type CreateRoomRequest struct { func (x *CreateRoomRequest) Reset() { *x = CreateRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[62] + mi := &file_proto_room_v1_room_proto_msgTypes[72] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5240,7 +6147,7 @@ func (x *CreateRoomRequest) String() string { func (*CreateRoomRequest) ProtoMessage() {} func (x *CreateRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[62] + mi := &file_proto_room_v1_room_proto_msgTypes[72] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5253,7 +6160,7 @@ func (x *CreateRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateRoomRequest.ProtoReflect.Descriptor instead. func (*CreateRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{62} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{72} } func (x *CreateRoomRequest) GetMeta() *RequestMeta { @@ -5344,7 +6251,7 @@ type CreateRoomResponse struct { func (x *CreateRoomResponse) Reset() { *x = CreateRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[63] + mi := &file_proto_room_v1_room_proto_msgTypes[73] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5356,7 +6263,7 @@ func (x *CreateRoomResponse) String() string { func (*CreateRoomResponse) ProtoMessage() {} func (x *CreateRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[63] + mi := &file_proto_room_v1_room_proto_msgTypes[73] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5369,7 +6276,7 @@ func (x *CreateRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CreateRoomResponse.ProtoReflect.Descriptor instead. func (*CreateRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{63} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{73} } func (x *CreateRoomResponse) GetResult() *CommandResult { @@ -5401,7 +6308,7 @@ type UpdateRoomProfileRequest struct { func (x *UpdateRoomProfileRequest) Reset() { *x = UpdateRoomProfileRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[64] + mi := &file_proto_room_v1_room_proto_msgTypes[74] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5413,7 +6320,7 @@ func (x *UpdateRoomProfileRequest) String() string { func (*UpdateRoomProfileRequest) ProtoMessage() {} func (x *UpdateRoomProfileRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[64] + mi := &file_proto_room_v1_room_proto_msgTypes[74] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5426,7 +6333,7 @@ func (x *UpdateRoomProfileRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRoomProfileRequest.ProtoReflect.Descriptor instead. func (*UpdateRoomProfileRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{64} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{74} } func (x *UpdateRoomProfileRequest) GetMeta() *RequestMeta { @@ -5475,7 +6382,7 @@ type UpdateRoomProfileResponse struct { func (x *UpdateRoomProfileResponse) Reset() { *x = UpdateRoomProfileResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[65] + mi := &file_proto_room_v1_room_proto_msgTypes[75] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5487,7 +6394,7 @@ func (x *UpdateRoomProfileResponse) String() string { func (*UpdateRoomProfileResponse) ProtoMessage() {} func (x *UpdateRoomProfileResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[65] + mi := &file_proto_room_v1_room_proto_msgTypes[75] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5500,7 +6407,7 @@ func (x *UpdateRoomProfileResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateRoomProfileResponse.ProtoReflect.Descriptor instead. func (*UpdateRoomProfileResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{65} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{75} } func (x *UpdateRoomProfileResponse) GetResult() *CommandResult { @@ -5535,7 +6442,7 @@ type RoomEntryVehicleSnapshot struct { func (x *RoomEntryVehicleSnapshot) Reset() { *x = RoomEntryVehicleSnapshot{} - mi := &file_proto_room_v1_room_proto_msgTypes[66] + mi := &file_proto_room_v1_room_proto_msgTypes[76] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5547,7 +6454,7 @@ func (x *RoomEntryVehicleSnapshot) String() string { func (*RoomEntryVehicleSnapshot) ProtoMessage() {} func (x *RoomEntryVehicleSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[66] + mi := &file_proto_room_v1_room_proto_msgTypes[76] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5560,7 +6467,7 @@ func (x *RoomEntryVehicleSnapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomEntryVehicleSnapshot.ProtoReflect.Descriptor instead. func (*RoomEntryVehicleSnapshot) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{66} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{76} } func (x *RoomEntryVehicleSnapshot) GetResourceId() int64 { @@ -5640,7 +6547,7 @@ type RoomUserDisplayProfile struct { func (x *RoomUserDisplayProfile) Reset() { *x = RoomUserDisplayProfile{} - mi := &file_proto_room_v1_room_proto_msgTypes[67] + mi := &file_proto_room_v1_room_proto_msgTypes[77] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5652,7 +6559,7 @@ func (x *RoomUserDisplayProfile) String() string { func (*RoomUserDisplayProfile) ProtoMessage() {} func (x *RoomUserDisplayProfile) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[67] + mi := &file_proto_room_v1_room_proto_msgTypes[77] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5665,7 +6572,7 @@ func (x *RoomUserDisplayProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomUserDisplayProfile.ProtoReflect.Descriptor instead. func (*RoomUserDisplayProfile) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{67} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{77} } func (x *RoomUserDisplayProfile) GetUserId() int64 { @@ -5727,7 +6634,7 @@ type JoinRoomRequest struct { func (x *JoinRoomRequest) Reset() { *x = JoinRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[68] + mi := &file_proto_room_v1_room_proto_msgTypes[78] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5739,7 +6646,7 @@ func (x *JoinRoomRequest) String() string { func (*JoinRoomRequest) ProtoMessage() {} func (x *JoinRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[68] + mi := &file_proto_room_v1_room_proto_msgTypes[78] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5752,7 +6659,7 @@ func (x *JoinRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use JoinRoomRequest.ProtoReflect.Descriptor instead. func (*JoinRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{68} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{78} } func (x *JoinRoomRequest) GetMeta() *RequestMeta { @@ -5839,7 +6746,7 @@ type RoomEffectiveVipSnapshot struct { func (x *RoomEffectiveVipSnapshot) Reset() { *x = RoomEffectiveVipSnapshot{} - mi := &file_proto_room_v1_room_proto_msgTypes[69] + mi := &file_proto_room_v1_room_proto_msgTypes[79] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5851,7 +6758,7 @@ func (x *RoomEffectiveVipSnapshot) String() string { func (*RoomEffectiveVipSnapshot) ProtoMessage() {} func (x *RoomEffectiveVipSnapshot) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[69] + mi := &file_proto_room_v1_room_proto_msgTypes[79] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5864,7 +6771,7 @@ func (x *RoomEffectiveVipSnapshot) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomEffectiveVipSnapshot.ProtoReflect.Descriptor instead. func (*RoomEffectiveVipSnapshot) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{69} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{79} } func (x *RoomEffectiveVipSnapshot) GetProgramType() string { @@ -5908,7 +6815,7 @@ type JoinRoomResponse struct { func (x *JoinRoomResponse) Reset() { *x = JoinRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[70] + mi := &file_proto_room_v1_room_proto_msgTypes[80] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5920,7 +6827,7 @@ func (x *JoinRoomResponse) String() string { func (*JoinRoomResponse) ProtoMessage() {} func (x *JoinRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[70] + mi := &file_proto_room_v1_room_proto_msgTypes[80] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5933,7 +6840,7 @@ func (x *JoinRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use JoinRoomResponse.ProtoReflect.Descriptor instead. func (*JoinRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{70} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{80} } func (x *JoinRoomResponse) GetResult() *CommandResult { @@ -5975,7 +6882,7 @@ type RoomHeartbeatRequest struct { func (x *RoomHeartbeatRequest) Reset() { *x = RoomHeartbeatRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[71] + mi := &file_proto_room_v1_room_proto_msgTypes[81] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -5987,7 +6894,7 @@ func (x *RoomHeartbeatRequest) String() string { func (*RoomHeartbeatRequest) ProtoMessage() {} func (x *RoomHeartbeatRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[71] + mi := &file_proto_room_v1_room_proto_msgTypes[81] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6000,7 +6907,7 @@ func (x *RoomHeartbeatRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomHeartbeatRequest.ProtoReflect.Descriptor instead. func (*RoomHeartbeatRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{71} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{81} } func (x *RoomHeartbeatRequest) GetMeta() *RequestMeta { @@ -6022,7 +6929,7 @@ type RoomHeartbeatResponse struct { func (x *RoomHeartbeatResponse) Reset() { *x = RoomHeartbeatResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[72] + mi := &file_proto_room_v1_room_proto_msgTypes[82] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6034,7 +6941,7 @@ func (x *RoomHeartbeatResponse) String() string { func (*RoomHeartbeatResponse) ProtoMessage() {} func (x *RoomHeartbeatResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[72] + mi := &file_proto_room_v1_room_proto_msgTypes[82] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6047,7 +6954,7 @@ func (x *RoomHeartbeatResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomHeartbeatResponse.ProtoReflect.Descriptor instead. func (*RoomHeartbeatResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{72} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{82} } func (x *RoomHeartbeatResponse) GetResult() *CommandResult { @@ -6081,7 +6988,7 @@ type LeaveRoomRequest struct { func (x *LeaveRoomRequest) Reset() { *x = LeaveRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[73] + mi := &file_proto_room_v1_room_proto_msgTypes[83] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6093,7 +7000,7 @@ func (x *LeaveRoomRequest) String() string { func (*LeaveRoomRequest) ProtoMessage() {} func (x *LeaveRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[73] + mi := &file_proto_room_v1_room_proto_msgTypes[83] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6106,7 +7013,7 @@ func (x *LeaveRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use LeaveRoomRequest.ProtoReflect.Descriptor instead. func (*LeaveRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{73} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{83} } func (x *LeaveRoomRequest) GetMeta() *RequestMeta { @@ -6127,7 +7034,7 @@ type LeaveRoomResponse struct { func (x *LeaveRoomResponse) Reset() { *x = LeaveRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[74] + mi := &file_proto_room_v1_room_proto_msgTypes[84] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6139,7 +7046,7 @@ func (x *LeaveRoomResponse) String() string { func (*LeaveRoomResponse) ProtoMessage() {} func (x *LeaveRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[74] + mi := &file_proto_room_v1_room_proto_msgTypes[84] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6152,7 +7059,7 @@ func (x *LeaveRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use LeaveRoomResponse.ProtoReflect.Descriptor instead. func (*LeaveRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{74} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{84} } func (x *LeaveRoomResponse) GetResult() *CommandResult { @@ -6180,7 +7087,7 @@ type CloseRoomRequest struct { func (x *CloseRoomRequest) Reset() { *x = CloseRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[75] + mi := &file_proto_room_v1_room_proto_msgTypes[85] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6192,7 +7099,7 @@ func (x *CloseRoomRequest) String() string { func (*CloseRoomRequest) ProtoMessage() {} func (x *CloseRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[75] + mi := &file_proto_room_v1_room_proto_msgTypes[85] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6205,7 +7112,7 @@ func (x *CloseRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CloseRoomRequest.ProtoReflect.Descriptor instead. func (*CloseRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{75} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{85} } func (x *CloseRoomRequest) GetMeta() *RequestMeta { @@ -6233,7 +7140,7 @@ type CloseRoomResponse struct { func (x *CloseRoomResponse) Reset() { *x = CloseRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[76] + mi := &file_proto_room_v1_room_proto_msgTypes[86] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6245,7 +7152,7 @@ func (x *CloseRoomResponse) String() string { func (*CloseRoomResponse) ProtoMessage() {} func (x *CloseRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[76] + mi := &file_proto_room_v1_room_proto_msgTypes[86] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6258,7 +7165,7 @@ func (x *CloseRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CloseRoomResponse.ProtoReflect.Descriptor instead. func (*CloseRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{76} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{86} } func (x *CloseRoomResponse) GetResult() *CommandResult { @@ -6303,7 +7210,7 @@ type AdminRoomListItem struct { func (x *AdminRoomListItem) Reset() { *x = AdminRoomListItem{} - mi := &file_proto_room_v1_room_proto_msgTypes[77] + mi := &file_proto_room_v1_room_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6315,7 +7222,7 @@ func (x *AdminRoomListItem) String() string { func (*AdminRoomListItem) ProtoMessage() {} func (x *AdminRoomListItem) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[77] + mi := &file_proto_room_v1_room_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6328,7 +7235,7 @@ func (x *AdminRoomListItem) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminRoomListItem.ProtoReflect.Descriptor instead. func (*AdminRoomListItem) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{77} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{87} } func (x *AdminRoomListItem) GetRoomId() string { @@ -6481,7 +7388,7 @@ type AdminListRoomsRequest struct { func (x *AdminListRoomsRequest) Reset() { *x = AdminListRoomsRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[78] + mi := &file_proto_room_v1_room_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6493,7 +7400,7 @@ func (x *AdminListRoomsRequest) String() string { func (*AdminListRoomsRequest) ProtoMessage() {} func (x *AdminListRoomsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[78] + mi := &file_proto_room_v1_room_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6506,7 +7413,7 @@ func (x *AdminListRoomsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminListRoomsRequest.ProtoReflect.Descriptor instead. func (*AdminListRoomsRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{78} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{88} } func (x *AdminListRoomsRequest) GetMeta() *RequestMeta { @@ -6583,7 +7490,7 @@ type AdminListRoomsResponse struct { func (x *AdminListRoomsResponse) Reset() { *x = AdminListRoomsResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[79] + mi := &file_proto_room_v1_room_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6595,7 +7502,7 @@ func (x *AdminListRoomsResponse) String() string { func (*AdminListRoomsResponse) ProtoMessage() {} func (x *AdminListRoomsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[79] + mi := &file_proto_room_v1_room_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6608,7 +7515,7 @@ func (x *AdminListRoomsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminListRoomsResponse.ProtoReflect.Descriptor instead. func (*AdminListRoomsResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{79} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{89} } func (x *AdminListRoomsResponse) GetRooms() []*AdminRoomListItem { @@ -6642,7 +7549,7 @@ type AdminGetRoomRequest struct { func (x *AdminGetRoomRequest) Reset() { *x = AdminGetRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[80] + mi := &file_proto_room_v1_room_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6654,7 +7561,7 @@ func (x *AdminGetRoomRequest) String() string { func (*AdminGetRoomRequest) ProtoMessage() {} func (x *AdminGetRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[80] + mi := &file_proto_room_v1_room_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6667,7 +7574,7 @@ func (x *AdminGetRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminGetRoomRequest.ProtoReflect.Descriptor instead. func (*AdminGetRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{80} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{90} } func (x *AdminGetRoomRequest) GetMeta() *RequestMeta { @@ -6694,7 +7601,7 @@ type AdminGetRoomResponse struct { func (x *AdminGetRoomResponse) Reset() { *x = AdminGetRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[81] + mi := &file_proto_room_v1_room_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6706,7 +7613,7 @@ func (x *AdminGetRoomResponse) String() string { func (*AdminGetRoomResponse) ProtoMessage() {} func (x *AdminGetRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[81] + mi := &file_proto_room_v1_room_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6719,7 +7626,7 @@ func (x *AdminGetRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminGetRoomResponse.ProtoReflect.Descriptor instead. func (*AdminGetRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{81} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{91} } func (x *AdminGetRoomResponse) GetRoom() *AdminRoomListItem { @@ -6756,7 +7663,7 @@ type AdminUpdateRoomRequest struct { func (x *AdminUpdateRoomRequest) Reset() { *x = AdminUpdateRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[82] + mi := &file_proto_room_v1_room_proto_msgTypes[92] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6768,7 +7675,7 @@ func (x *AdminUpdateRoomRequest) String() string { func (*AdminUpdateRoomRequest) ProtoMessage() {} func (x *AdminUpdateRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[82] + mi := &file_proto_room_v1_room_proto_msgTypes[92] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6781,7 +7688,7 @@ func (x *AdminUpdateRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminUpdateRoomRequest.ProtoReflect.Descriptor instead. func (*AdminUpdateRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{82} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{92} } func (x *AdminUpdateRoomRequest) GetMeta() *RequestMeta { @@ -6871,7 +7778,7 @@ type AdminUpdateRoomResponse struct { func (x *AdminUpdateRoomResponse) Reset() { *x = AdminUpdateRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[83] + mi := &file_proto_room_v1_room_proto_msgTypes[93] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6883,7 +7790,7 @@ func (x *AdminUpdateRoomResponse) String() string { func (*AdminUpdateRoomResponse) ProtoMessage() {} func (x *AdminUpdateRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[83] + mi := &file_proto_room_v1_room_proto_msgTypes[93] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6896,7 +7803,7 @@ func (x *AdminUpdateRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminUpdateRoomResponse.ProtoReflect.Descriptor instead. func (*AdminUpdateRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{83} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{93} } func (x *AdminUpdateRoomResponse) GetResult() *CommandResult { @@ -6924,7 +7831,7 @@ type AdminDeleteRoomRequest struct { func (x *AdminDeleteRoomRequest) Reset() { *x = AdminDeleteRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[84] + mi := &file_proto_room_v1_room_proto_msgTypes[94] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6936,7 +7843,7 @@ func (x *AdminDeleteRoomRequest) String() string { func (*AdminDeleteRoomRequest) ProtoMessage() {} func (x *AdminDeleteRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[84] + mi := &file_proto_room_v1_room_proto_msgTypes[94] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -6949,7 +7856,7 @@ func (x *AdminDeleteRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminDeleteRoomRequest.ProtoReflect.Descriptor instead. func (*AdminDeleteRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{84} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{94} } func (x *AdminDeleteRoomRequest) GetMeta() *RequestMeta { @@ -6983,7 +7890,7 @@ type AdminDeleteRoomResponse struct { func (x *AdminDeleteRoomResponse) Reset() { *x = AdminDeleteRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[85] + mi := &file_proto_room_v1_room_proto_msgTypes[95] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -6995,7 +7902,7 @@ func (x *AdminDeleteRoomResponse) String() string { func (*AdminDeleteRoomResponse) ProtoMessage() {} func (x *AdminDeleteRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[85] + mi := &file_proto_room_v1_room_proto_msgTypes[95] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7008,7 +7915,7 @@ func (x *AdminDeleteRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminDeleteRoomResponse.ProtoReflect.Descriptor instead. func (*AdminDeleteRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{85} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{95} } func (x *AdminDeleteRoomResponse) GetResult() *CommandResult { @@ -7038,7 +7945,7 @@ type MicUpRequest struct { func (x *MicUpRequest) Reset() { *x = MicUpRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[86] + mi := &file_proto_room_v1_room_proto_msgTypes[96] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7050,7 +7957,7 @@ func (x *MicUpRequest) String() string { func (*MicUpRequest) ProtoMessage() {} func (x *MicUpRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[86] + mi := &file_proto_room_v1_room_proto_msgTypes[96] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7063,7 +7970,7 @@ func (x *MicUpRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MicUpRequest.ProtoReflect.Descriptor instead. func (*MicUpRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{86} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{96} } func (x *MicUpRequest) GetMeta() *RequestMeta { @@ -7101,7 +8008,7 @@ type MicUpResponse struct { func (x *MicUpResponse) Reset() { *x = MicUpResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[87] + mi := &file_proto_room_v1_room_proto_msgTypes[97] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7113,7 +8020,7 @@ func (x *MicUpResponse) String() string { func (*MicUpResponse) ProtoMessage() {} func (x *MicUpResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[87] + mi := &file_proto_room_v1_room_proto_msgTypes[97] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7126,7 +8033,7 @@ func (x *MicUpResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MicUpResponse.ProtoReflect.Descriptor instead. func (*MicUpResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{87} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{97} } func (x *MicUpResponse) GetResult() *CommandResult { @@ -7179,7 +8086,7 @@ type MicDownRequest struct { func (x *MicDownRequest) Reset() { *x = MicDownRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[88] + mi := &file_proto_room_v1_room_proto_msgTypes[98] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7191,7 +8098,7 @@ func (x *MicDownRequest) String() string { func (*MicDownRequest) ProtoMessage() {} func (x *MicDownRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[88] + mi := &file_proto_room_v1_room_proto_msgTypes[98] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7204,7 +8111,7 @@ func (x *MicDownRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MicDownRequest.ProtoReflect.Descriptor instead. func (*MicDownRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{88} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{98} } func (x *MicDownRequest) GetMeta() *RequestMeta { @@ -7247,7 +8154,7 @@ type MicDownResponse struct { func (x *MicDownResponse) Reset() { *x = MicDownResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[89] + mi := &file_proto_room_v1_room_proto_msgTypes[99] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7259,7 +8166,7 @@ func (x *MicDownResponse) String() string { func (*MicDownResponse) ProtoMessage() {} func (x *MicDownResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[89] + mi := &file_proto_room_v1_room_proto_msgTypes[99] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7272,7 +8179,7 @@ func (x *MicDownResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MicDownResponse.ProtoReflect.Descriptor instead. func (*MicDownResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{89} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{99} } func (x *MicDownResponse) GetResult() *CommandResult { @@ -7310,7 +8217,7 @@ type ChangeMicSeatRequest struct { func (x *ChangeMicSeatRequest) Reset() { *x = ChangeMicSeatRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[90] + mi := &file_proto_room_v1_room_proto_msgTypes[100] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7322,7 +8229,7 @@ func (x *ChangeMicSeatRequest) String() string { func (*ChangeMicSeatRequest) ProtoMessage() {} func (x *ChangeMicSeatRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[90] + mi := &file_proto_room_v1_room_proto_msgTypes[100] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7335,7 +8242,7 @@ func (x *ChangeMicSeatRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeMicSeatRequest.ProtoReflect.Descriptor instead. func (*ChangeMicSeatRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{90} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{100} } func (x *ChangeMicSeatRequest) GetMeta() *RequestMeta { @@ -7377,7 +8284,7 @@ type ChangeMicSeatResponse struct { func (x *ChangeMicSeatResponse) Reset() { *x = ChangeMicSeatResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[91] + mi := &file_proto_room_v1_room_proto_msgTypes[101] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7389,7 +8296,7 @@ func (x *ChangeMicSeatResponse) String() string { func (*ChangeMicSeatResponse) ProtoMessage() {} func (x *ChangeMicSeatResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[91] + mi := &file_proto_room_v1_room_proto_msgTypes[101] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7402,7 +8309,7 @@ func (x *ChangeMicSeatResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ChangeMicSeatResponse.ProtoReflect.Descriptor instead. func (*ChangeMicSeatResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{91} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{101} } func (x *ChangeMicSeatResponse) GetResult() *CommandResult { @@ -7438,7 +8345,7 @@ type ConfirmMicPublishingRequest struct { func (x *ConfirmMicPublishingRequest) Reset() { *x = ConfirmMicPublishingRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[92] + mi := &file_proto_room_v1_room_proto_msgTypes[102] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7450,7 +8357,7 @@ func (x *ConfirmMicPublishingRequest) String() string { func (*ConfirmMicPublishingRequest) ProtoMessage() {} func (x *ConfirmMicPublishingRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[92] + mi := &file_proto_room_v1_room_proto_msgTypes[102] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7463,7 +8370,7 @@ func (x *ConfirmMicPublishingRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfirmMicPublishingRequest.ProtoReflect.Descriptor instead. func (*ConfirmMicPublishingRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{92} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{102} } func (x *ConfirmMicPublishingRequest) GetMeta() *RequestMeta { @@ -7527,7 +8434,7 @@ type ConfirmMicPublishingResponse struct { func (x *ConfirmMicPublishingResponse) Reset() { *x = ConfirmMicPublishingResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[93] + mi := &file_proto_room_v1_room_proto_msgTypes[103] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7539,7 +8446,7 @@ func (x *ConfirmMicPublishingResponse) String() string { func (*ConfirmMicPublishingResponse) ProtoMessage() {} func (x *ConfirmMicPublishingResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[93] + mi := &file_proto_room_v1_room_proto_msgTypes[103] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7552,7 +8459,7 @@ func (x *ConfirmMicPublishingResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ConfirmMicPublishingResponse.ProtoReflect.Descriptor instead. func (*ConfirmMicPublishingResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{93} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{103} } func (x *ConfirmMicPublishingResponse) GetResult() *CommandResult { @@ -7590,7 +8497,7 @@ type MicHeartbeatRequest struct { func (x *MicHeartbeatRequest) Reset() { *x = MicHeartbeatRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[94] + mi := &file_proto_room_v1_room_proto_msgTypes[104] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7602,7 +8509,7 @@ func (x *MicHeartbeatRequest) String() string { func (*MicHeartbeatRequest) ProtoMessage() {} func (x *MicHeartbeatRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[94] + mi := &file_proto_room_v1_room_proto_msgTypes[104] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7615,7 +8522,7 @@ func (x *MicHeartbeatRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MicHeartbeatRequest.ProtoReflect.Descriptor instead. func (*MicHeartbeatRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{94} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{104} } func (x *MicHeartbeatRequest) GetMeta() *RequestMeta { @@ -7652,7 +8559,7 @@ type MicHeartbeatResponse struct { func (x *MicHeartbeatResponse) Reset() { *x = MicHeartbeatResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[95] + mi := &file_proto_room_v1_room_proto_msgTypes[105] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7664,7 +8571,7 @@ func (x *MicHeartbeatResponse) String() string { func (*MicHeartbeatResponse) ProtoMessage() {} func (x *MicHeartbeatResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[95] + mi := &file_proto_room_v1_room_proto_msgTypes[105] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7677,7 +8584,7 @@ func (x *MicHeartbeatResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MicHeartbeatResponse.ProtoReflect.Descriptor instead. func (*MicHeartbeatResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{95} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{105} } func (x *MicHeartbeatResponse) GetResult() *CommandResult { @@ -7723,7 +8630,7 @@ type SetMicMuteRequest struct { func (x *SetMicMuteRequest) Reset() { *x = SetMicMuteRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[96] + mi := &file_proto_room_v1_room_proto_msgTypes[106] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7735,7 +8642,7 @@ func (x *SetMicMuteRequest) String() string { func (*SetMicMuteRequest) ProtoMessage() {} func (x *SetMicMuteRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[96] + mi := &file_proto_room_v1_room_proto_msgTypes[106] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7748,7 +8655,7 @@ func (x *SetMicMuteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetMicMuteRequest.ProtoReflect.Descriptor instead. func (*SetMicMuteRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{96} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{106} } func (x *SetMicMuteRequest) GetMeta() *RequestMeta { @@ -7791,7 +8698,7 @@ type SetMicMuteResponse struct { func (x *SetMicMuteResponse) Reset() { *x = SetMicMuteResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[97] + mi := &file_proto_room_v1_room_proto_msgTypes[107] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7803,7 +8710,7 @@ func (x *SetMicMuteResponse) String() string { func (*SetMicMuteResponse) ProtoMessage() {} func (x *SetMicMuteResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[97] + mi := &file_proto_room_v1_room_proto_msgTypes[107] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7816,7 +8723,7 @@ func (x *SetMicMuteResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetMicMuteResponse.ProtoReflect.Descriptor instead. func (*SetMicMuteResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{97} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{107} } func (x *SetMicMuteResponse) GetResult() *CommandResult { @@ -7857,7 +8764,7 @@ type ApplyRTCEventRequest struct { func (x *ApplyRTCEventRequest) Reset() { *x = ApplyRTCEventRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[98] + mi := &file_proto_room_v1_room_proto_msgTypes[108] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7869,7 +8776,7 @@ func (x *ApplyRTCEventRequest) String() string { func (*ApplyRTCEventRequest) ProtoMessage() {} func (x *ApplyRTCEventRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[98] + mi := &file_proto_room_v1_room_proto_msgTypes[108] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7882,7 +8789,7 @@ func (x *ApplyRTCEventRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyRTCEventRequest.ProtoReflect.Descriptor instead. func (*ApplyRTCEventRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{98} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{108} } func (x *ApplyRTCEventRequest) GetMeta() *RequestMeta { @@ -7946,7 +8853,7 @@ type ApplyRTCEventResponse struct { func (x *ApplyRTCEventResponse) Reset() { *x = ApplyRTCEventResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[99] + mi := &file_proto_room_v1_room_proto_msgTypes[109] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -7958,7 +8865,7 @@ func (x *ApplyRTCEventResponse) String() string { func (*ApplyRTCEventResponse) ProtoMessage() {} func (x *ApplyRTCEventResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[99] + mi := &file_proto_room_v1_room_proto_msgTypes[109] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -7971,7 +8878,7 @@ func (x *ApplyRTCEventResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ApplyRTCEventResponse.ProtoReflect.Descriptor instead. func (*ApplyRTCEventResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{99} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{109} } func (x *ApplyRTCEventResponse) GetResult() *CommandResult { @@ -8007,7 +8914,7 @@ type SetMicSeatLockRequest struct { func (x *SetMicSeatLockRequest) Reset() { *x = SetMicSeatLockRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[100] + mi := &file_proto_room_v1_room_proto_msgTypes[110] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8019,7 +8926,7 @@ func (x *SetMicSeatLockRequest) String() string { func (*SetMicSeatLockRequest) ProtoMessage() {} func (x *SetMicSeatLockRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[100] + mi := &file_proto_room_v1_room_proto_msgTypes[110] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8032,7 +8939,7 @@ func (x *SetMicSeatLockRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetMicSeatLockRequest.ProtoReflect.Descriptor instead. func (*SetMicSeatLockRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{100} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{110} } func (x *SetMicSeatLockRequest) GetMeta() *RequestMeta { @@ -8067,7 +8974,7 @@ type SetMicSeatLockResponse struct { func (x *SetMicSeatLockResponse) Reset() { *x = SetMicSeatLockResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[101] + mi := &file_proto_room_v1_room_proto_msgTypes[111] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8079,7 +8986,7 @@ func (x *SetMicSeatLockResponse) String() string { func (*SetMicSeatLockResponse) ProtoMessage() {} func (x *SetMicSeatLockResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[101] + mi := &file_proto_room_v1_room_proto_msgTypes[111] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8092,7 +8999,7 @@ func (x *SetMicSeatLockResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetMicSeatLockResponse.ProtoReflect.Descriptor instead. func (*SetMicSeatLockResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{101} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{111} } func (x *SetMicSeatLockResponse) GetResult() *CommandResult { @@ -8120,7 +9027,7 @@ type SetChatEnabledRequest struct { func (x *SetChatEnabledRequest) Reset() { *x = SetChatEnabledRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[102] + mi := &file_proto_room_v1_room_proto_msgTypes[112] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8132,7 +9039,7 @@ func (x *SetChatEnabledRequest) String() string { func (*SetChatEnabledRequest) ProtoMessage() {} func (x *SetChatEnabledRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[102] + mi := &file_proto_room_v1_room_proto_msgTypes[112] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8145,7 +9052,7 @@ func (x *SetChatEnabledRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetChatEnabledRequest.ProtoReflect.Descriptor instead. func (*SetChatEnabledRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{102} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{112} } func (x *SetChatEnabledRequest) GetMeta() *RequestMeta { @@ -8173,7 +9080,7 @@ type SetChatEnabledResponse struct { func (x *SetChatEnabledResponse) Reset() { *x = SetChatEnabledResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[103] + mi := &file_proto_room_v1_room_proto_msgTypes[113] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8185,7 +9092,7 @@ func (x *SetChatEnabledResponse) String() string { func (*SetChatEnabledResponse) ProtoMessage() {} func (x *SetChatEnabledResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[103] + mi := &file_proto_room_v1_room_proto_msgTypes[113] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8198,7 +9105,7 @@ func (x *SetChatEnabledResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetChatEnabledResponse.ProtoReflect.Descriptor instead. func (*SetChatEnabledResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{103} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{113} } func (x *SetChatEnabledResponse) GetResult() *CommandResult { @@ -8228,7 +9135,7 @@ type SetRoomPasswordRequest struct { func (x *SetRoomPasswordRequest) Reset() { *x = SetRoomPasswordRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[104] + mi := &file_proto_room_v1_room_proto_msgTypes[114] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8240,7 +9147,7 @@ func (x *SetRoomPasswordRequest) String() string { func (*SetRoomPasswordRequest) ProtoMessage() {} func (x *SetRoomPasswordRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[104] + mi := &file_proto_room_v1_room_proto_msgTypes[114] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8253,7 +9160,7 @@ func (x *SetRoomPasswordRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRoomPasswordRequest.ProtoReflect.Descriptor instead. func (*SetRoomPasswordRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{104} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{114} } func (x *SetRoomPasswordRequest) GetMeta() *RequestMeta { @@ -8288,7 +9195,7 @@ type SetRoomPasswordResponse struct { func (x *SetRoomPasswordResponse) Reset() { *x = SetRoomPasswordResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[105] + mi := &file_proto_room_v1_room_proto_msgTypes[115] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8300,7 +9207,7 @@ func (x *SetRoomPasswordResponse) String() string { func (*SetRoomPasswordResponse) ProtoMessage() {} func (x *SetRoomPasswordResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[105] + mi := &file_proto_room_v1_room_proto_msgTypes[115] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8313,7 +9220,7 @@ func (x *SetRoomPasswordResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRoomPasswordResponse.ProtoReflect.Descriptor instead. func (*SetRoomPasswordResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{105} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{115} } func (x *SetRoomPasswordResponse) GetResult() *CommandResult { @@ -8342,7 +9249,7 @@ type SetRoomAdminRequest struct { func (x *SetRoomAdminRequest) Reset() { *x = SetRoomAdminRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[106] + mi := &file_proto_room_v1_room_proto_msgTypes[116] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8354,7 +9261,7 @@ func (x *SetRoomAdminRequest) String() string { func (*SetRoomAdminRequest) ProtoMessage() {} func (x *SetRoomAdminRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[106] + mi := &file_proto_room_v1_room_proto_msgTypes[116] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8367,7 +9274,7 @@ func (x *SetRoomAdminRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRoomAdminRequest.ProtoReflect.Descriptor instead. func (*SetRoomAdminRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{106} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{116} } func (x *SetRoomAdminRequest) GetMeta() *RequestMeta { @@ -8402,7 +9309,7 @@ type SetRoomAdminResponse struct { func (x *SetRoomAdminResponse) Reset() { *x = SetRoomAdminResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[107] + mi := &file_proto_room_v1_room_proto_msgTypes[117] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8414,7 +9321,7 @@ func (x *SetRoomAdminResponse) String() string { func (*SetRoomAdminResponse) ProtoMessage() {} func (x *SetRoomAdminResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[107] + mi := &file_proto_room_v1_room_proto_msgTypes[117] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8427,7 +9334,7 @@ func (x *SetRoomAdminResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SetRoomAdminResponse.ProtoReflect.Descriptor instead. func (*SetRoomAdminResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{107} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{117} } func (x *SetRoomAdminResponse) GetResult() *CommandResult { @@ -8456,7 +9363,7 @@ type MuteUserRequest struct { func (x *MuteUserRequest) Reset() { *x = MuteUserRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[108] + mi := &file_proto_room_v1_room_proto_msgTypes[118] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8468,7 +9375,7 @@ func (x *MuteUserRequest) String() string { func (*MuteUserRequest) ProtoMessage() {} func (x *MuteUserRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[108] + mi := &file_proto_room_v1_room_proto_msgTypes[118] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8481,7 +9388,7 @@ func (x *MuteUserRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use MuteUserRequest.ProtoReflect.Descriptor instead. func (*MuteUserRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{108} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{118} } func (x *MuteUserRequest) GetMeta() *RequestMeta { @@ -8516,7 +9423,7 @@ type MuteUserResponse struct { func (x *MuteUserResponse) Reset() { *x = MuteUserResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[109] + mi := &file_proto_room_v1_room_proto_msgTypes[119] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8528,7 +9435,7 @@ func (x *MuteUserResponse) String() string { func (*MuteUserResponse) ProtoMessage() {} func (x *MuteUserResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[109] + mi := &file_proto_room_v1_room_proto_msgTypes[119] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8541,7 +9448,7 @@ func (x *MuteUserResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use MuteUserResponse.ProtoReflect.Descriptor instead. func (*MuteUserResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{109} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{119} } func (x *MuteUserResponse) GetResult() *CommandResult { @@ -8571,7 +9478,7 @@ type KickUserRequest struct { func (x *KickUserRequest) Reset() { *x = KickUserRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[110] + mi := &file_proto_room_v1_room_proto_msgTypes[120] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8583,7 +9490,7 @@ func (x *KickUserRequest) String() string { func (*KickUserRequest) ProtoMessage() {} func (x *KickUserRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[110] + mi := &file_proto_room_v1_room_proto_msgTypes[120] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8596,7 +9503,7 @@ func (x *KickUserRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use KickUserRequest.ProtoReflect.Descriptor instead. func (*KickUserRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{110} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{120} } func (x *KickUserRequest) GetMeta() *RequestMeta { @@ -8635,7 +9542,7 @@ type KickUserResponse struct { func (x *KickUserResponse) Reset() { *x = KickUserResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[111] + mi := &file_proto_room_v1_room_proto_msgTypes[121] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8647,7 +9554,7 @@ func (x *KickUserResponse) String() string { func (*KickUserResponse) ProtoMessage() {} func (x *KickUserResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[111] + mi := &file_proto_room_v1_room_proto_msgTypes[121] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8660,7 +9567,7 @@ func (x *KickUserResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use KickUserResponse.ProtoReflect.Descriptor instead. func (*KickUserResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{111} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{121} } func (x *KickUserResponse) GetResult() *CommandResult { @@ -8702,7 +9609,7 @@ type UnbanUserRequest struct { func (x *UnbanUserRequest) Reset() { *x = UnbanUserRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[112] + mi := &file_proto_room_v1_room_proto_msgTypes[122] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8714,7 +9621,7 @@ func (x *UnbanUserRequest) String() string { func (*UnbanUserRequest) ProtoMessage() {} func (x *UnbanUserRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[112] + mi := &file_proto_room_v1_room_proto_msgTypes[122] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8727,7 +9634,7 @@ func (x *UnbanUserRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UnbanUserRequest.ProtoReflect.Descriptor instead. func (*UnbanUserRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{112} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{122} } func (x *UnbanUserRequest) GetMeta() *RequestMeta { @@ -8755,7 +9662,7 @@ type UnbanUserResponse struct { func (x *UnbanUserResponse) Reset() { *x = UnbanUserResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[113] + mi := &file_proto_room_v1_room_proto_msgTypes[123] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8767,7 +9674,7 @@ func (x *UnbanUserResponse) String() string { func (*UnbanUserResponse) ProtoMessage() {} func (x *UnbanUserResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[113] + mi := &file_proto_room_v1_room_proto_msgTypes[123] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8780,7 +9687,7 @@ func (x *UnbanUserResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UnbanUserResponse.ProtoReflect.Descriptor instead. func (*UnbanUserResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{113} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{123} } func (x *UnbanUserResponse) GetResult() *CommandResult { @@ -8813,7 +9720,7 @@ type SystemEvictUserRequest struct { func (x *SystemEvictUserRequest) Reset() { *x = SystemEvictUserRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[114] + mi := &file_proto_room_v1_room_proto_msgTypes[124] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8825,7 +9732,7 @@ func (x *SystemEvictUserRequest) String() string { func (*SystemEvictUserRequest) ProtoMessage() {} func (x *SystemEvictUserRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[114] + mi := &file_proto_room_v1_room_proto_msgTypes[124] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8838,7 +9745,7 @@ func (x *SystemEvictUserRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemEvictUserRequest.ProtoReflect.Descriptor instead. func (*SystemEvictUserRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{114} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{124} } func (x *SystemEvictUserRequest) GetMeta() *RequestMeta { @@ -8891,7 +9798,7 @@ type SystemEvictUserResponse struct { func (x *SystemEvictUserResponse) Reset() { *x = SystemEvictUserResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[115] + mi := &file_proto_room_v1_room_proto_msgTypes[125] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8903,7 +9810,7 @@ func (x *SystemEvictUserResponse) String() string { func (*SystemEvictUserResponse) ProtoMessage() {} func (x *SystemEvictUserResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[115] + mi := &file_proto_room_v1_room_proto_msgTypes[125] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -8916,7 +9823,7 @@ func (x *SystemEvictUserResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SystemEvictUserResponse.ProtoReflect.Descriptor instead. func (*SystemEvictUserResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{115} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{125} } func (x *SystemEvictUserResponse) GetHadCurrentRoom() bool { @@ -8977,7 +9884,7 @@ type SendGiftTargetHostScope struct { func (x *SendGiftTargetHostScope) Reset() { *x = SendGiftTargetHostScope{} - mi := &file_proto_room_v1_room_proto_msgTypes[116] + mi := &file_proto_room_v1_room_proto_msgTypes[126] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -8989,7 +9896,7 @@ func (x *SendGiftTargetHostScope) String() string { func (*SendGiftTargetHostScope) ProtoMessage() {} func (x *SendGiftTargetHostScope) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[116] + mi := &file_proto_room_v1_room_proto_msgTypes[126] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9002,7 +9909,7 @@ func (x *SendGiftTargetHostScope) ProtoReflect() protoreflect.Message { // Deprecated: Use SendGiftTargetHostScope.ProtoReflect.Descriptor instead. func (*SendGiftTargetHostScope) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{116} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{126} } func (x *SendGiftTargetHostScope) GetTargetUserId() int64 { @@ -9048,7 +9955,7 @@ type SendGiftDisplayProfile struct { func (x *SendGiftDisplayProfile) Reset() { *x = SendGiftDisplayProfile{} - mi := &file_proto_room_v1_room_proto_msgTypes[117] + mi := &file_proto_room_v1_room_proto_msgTypes[127] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9060,7 +9967,7 @@ func (x *SendGiftDisplayProfile) String() string { func (*SendGiftDisplayProfile) ProtoMessage() {} func (x *SendGiftDisplayProfile) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[117] + mi := &file_proto_room_v1_room_proto_msgTypes[127] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9073,7 +9980,7 @@ func (x *SendGiftDisplayProfile) ProtoReflect() protoreflect.Message { // Deprecated: Use SendGiftDisplayProfile.ProtoReflect.Descriptor instead. func (*SendGiftDisplayProfile) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{117} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{127} } func (x *SendGiftDisplayProfile) GetUserId() int64 { @@ -9132,7 +10039,7 @@ type SendGiftBatchTarget struct { func (x *SendGiftBatchTarget) Reset() { *x = SendGiftBatchTarget{} - mi := &file_proto_room_v1_room_proto_msgTypes[118] + mi := &file_proto_room_v1_room_proto_msgTypes[128] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9144,7 +10051,7 @@ func (x *SendGiftBatchTarget) String() string { func (*SendGiftBatchTarget) ProtoMessage() {} func (x *SendGiftBatchTarget) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[118] + mi := &file_proto_room_v1_room_proto_msgTypes[128] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9157,7 +10064,7 @@ func (x *SendGiftBatchTarget) ProtoReflect() protoreflect.Message { // Deprecated: Use SendGiftBatchTarget.ProtoReflect.Descriptor instead. func (*SendGiftBatchTarget) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{118} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{128} } func (x *SendGiftBatchTarget) GetTargetUserId() int64 { @@ -9269,7 +10176,7 @@ type SendGiftBatchDisplay struct { func (x *SendGiftBatchDisplay) Reset() { *x = SendGiftBatchDisplay{} - mi := &file_proto_room_v1_room_proto_msgTypes[119] + mi := &file_proto_room_v1_room_proto_msgTypes[129] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9281,7 +10188,7 @@ func (x *SendGiftBatchDisplay) String() string { func (*SendGiftBatchDisplay) ProtoMessage() {} func (x *SendGiftBatchDisplay) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[119] + mi := &file_proto_room_v1_room_proto_msgTypes[129] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9294,7 +10201,7 @@ func (x *SendGiftBatchDisplay) ProtoReflect() protoreflect.Message { // Deprecated: Use SendGiftBatchDisplay.ProtoReflect.Descriptor instead. func (*SendGiftBatchDisplay) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{119} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{129} } func (x *SendGiftBatchDisplay) GetEventId() string { @@ -9497,7 +10404,7 @@ type SendGiftRequest struct { func (x *SendGiftRequest) Reset() { *x = SendGiftRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[120] + mi := &file_proto_room_v1_room_proto_msgTypes[130] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9509,7 +10416,7 @@ func (x *SendGiftRequest) String() string { func (*SendGiftRequest) ProtoMessage() {} func (x *SendGiftRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[120] + mi := &file_proto_room_v1_room_proto_msgTypes[130] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9522,7 +10429,7 @@ func (x *SendGiftRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use SendGiftRequest.ProtoReflect.Descriptor instead. func (*SendGiftRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{120} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{130} } func (x *SendGiftRequest) GetMeta() *RequestMeta { @@ -9699,7 +10606,7 @@ type SendGiftResponse struct { func (x *SendGiftResponse) Reset() { *x = SendGiftResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[121] + mi := &file_proto_room_v1_room_proto_msgTypes[131] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9711,7 +10618,7 @@ func (x *SendGiftResponse) String() string { func (*SendGiftResponse) ProtoMessage() {} func (x *SendGiftResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[121] + mi := &file_proto_room_v1_room_proto_msgTypes[131] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9724,7 +10631,7 @@ func (x *SendGiftResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use SendGiftResponse.ProtoReflect.Descriptor instead. func (*SendGiftResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{121} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{131} } func (x *SendGiftResponse) GetResult() *CommandResult { @@ -9823,7 +10730,7 @@ type CheckSpeakPermissionRequest struct { func (x *CheckSpeakPermissionRequest) Reset() { *x = CheckSpeakPermissionRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[122] + mi := &file_proto_room_v1_room_proto_msgTypes[132] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9835,7 +10742,7 @@ func (x *CheckSpeakPermissionRequest) String() string { func (*CheckSpeakPermissionRequest) ProtoMessage() {} func (x *CheckSpeakPermissionRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[122] + mi := &file_proto_room_v1_room_proto_msgTypes[132] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9848,7 +10755,7 @@ func (x *CheckSpeakPermissionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckSpeakPermissionRequest.ProtoReflect.Descriptor instead. func (*CheckSpeakPermissionRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{122} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{132} } func (x *CheckSpeakPermissionRequest) GetRoomId() string { @@ -9884,7 +10791,7 @@ type CheckSpeakPermissionResponse struct { func (x *CheckSpeakPermissionResponse) Reset() { *x = CheckSpeakPermissionResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[123] + mi := &file_proto_room_v1_room_proto_msgTypes[133] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9896,7 +10803,7 @@ func (x *CheckSpeakPermissionResponse) String() string { func (*CheckSpeakPermissionResponse) ProtoMessage() {} func (x *CheckSpeakPermissionResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[123] + mi := &file_proto_room_v1_room_proto_msgTypes[133] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9909,7 +10816,7 @@ func (x *CheckSpeakPermissionResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use CheckSpeakPermissionResponse.ProtoReflect.Descriptor instead. func (*CheckSpeakPermissionResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{123} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{133} } func (x *CheckSpeakPermissionResponse) GetAllowed() bool { @@ -9947,7 +10854,7 @@ type VerifyRoomPresenceRequest struct { func (x *VerifyRoomPresenceRequest) Reset() { *x = VerifyRoomPresenceRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[124] + mi := &file_proto_room_v1_room_proto_msgTypes[134] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -9959,7 +10866,7 @@ func (x *VerifyRoomPresenceRequest) String() string { func (*VerifyRoomPresenceRequest) ProtoMessage() {} func (x *VerifyRoomPresenceRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[124] + mi := &file_proto_room_v1_room_proto_msgTypes[134] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -9972,7 +10879,7 @@ func (x *VerifyRoomPresenceRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use VerifyRoomPresenceRequest.ProtoReflect.Descriptor instead. func (*VerifyRoomPresenceRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{124} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{134} } func (x *VerifyRoomPresenceRequest) GetRoomId() string { @@ -10022,7 +10929,7 @@ type VerifyRoomPresenceResponse struct { func (x *VerifyRoomPresenceResponse) Reset() { *x = VerifyRoomPresenceResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[125] + mi := &file_proto_room_v1_room_proto_msgTypes[135] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10034,7 +10941,7 @@ func (x *VerifyRoomPresenceResponse) String() string { func (*VerifyRoomPresenceResponse) ProtoMessage() {} func (x *VerifyRoomPresenceResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[125] + mi := &file_proto_room_v1_room_proto_msgTypes[135] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10047,7 +10954,7 @@ func (x *VerifyRoomPresenceResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use VerifyRoomPresenceResponse.ProtoReflect.Descriptor instead. func (*VerifyRoomPresenceResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{125} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{135} } func (x *VerifyRoomPresenceResponse) GetPresent() bool { @@ -10083,7 +10990,7 @@ type ResolveRoomAppCodeRequest struct { func (x *ResolveRoomAppCodeRequest) Reset() { *x = ResolveRoomAppCodeRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[126] + mi := &file_proto_room_v1_room_proto_msgTypes[136] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10095,7 +11002,7 @@ func (x *ResolveRoomAppCodeRequest) String() string { func (*ResolveRoomAppCodeRequest) ProtoMessage() {} func (x *ResolveRoomAppCodeRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[126] + mi := &file_proto_room_v1_room_proto_msgTypes[136] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10108,7 +11015,7 @@ func (x *ResolveRoomAppCodeRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ResolveRoomAppCodeRequest.ProtoReflect.Descriptor instead. func (*ResolveRoomAppCodeRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{126} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{136} } func (x *ResolveRoomAppCodeRequest) GetRequestId() string { @@ -10134,7 +11041,7 @@ type ResolveRoomAppCodeResponse struct { func (x *ResolveRoomAppCodeResponse) Reset() { *x = ResolveRoomAppCodeResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[127] + mi := &file_proto_room_v1_room_proto_msgTypes[137] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10146,7 +11053,7 @@ func (x *ResolveRoomAppCodeResponse) String() string { func (*ResolveRoomAppCodeResponse) ProtoMessage() {} func (x *ResolveRoomAppCodeResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[127] + mi := &file_proto_room_v1_room_proto_msgTypes[137] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10159,7 +11066,7 @@ func (x *ResolveRoomAppCodeResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ResolveRoomAppCodeResponse.ProtoReflect.Descriptor instead. func (*ResolveRoomAppCodeResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{127} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{137} } func (x *ResolveRoomAppCodeResponse) GetAppCode() string { @@ -10195,7 +11102,7 @@ type ListRoomsRequest struct { func (x *ListRoomsRequest) Reset() { *x = ListRoomsRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[128] + mi := &file_proto_room_v1_room_proto_msgTypes[138] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10207,7 +11114,7 @@ func (x *ListRoomsRequest) String() string { func (*ListRoomsRequest) ProtoMessage() {} func (x *ListRoomsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[128] + mi := &file_proto_room_v1_room_proto_msgTypes[138] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10220,7 +11127,7 @@ func (x *ListRoomsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomsRequest.ProtoReflect.Descriptor instead. func (*ListRoomsRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{128} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{138} } func (x *ListRoomsRequest) GetMeta() *RequestMeta { @@ -10317,7 +11224,7 @@ type ListRoomsByOwnersRequest struct { func (x *ListRoomsByOwnersRequest) Reset() { *x = ListRoomsByOwnersRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[129] + mi := &file_proto_room_v1_room_proto_msgTypes[139] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10329,7 +11236,7 @@ func (x *ListRoomsByOwnersRequest) String() string { func (*ListRoomsByOwnersRequest) ProtoMessage() {} func (x *ListRoomsByOwnersRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[129] + mi := &file_proto_room_v1_room_proto_msgTypes[139] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10342,7 +11249,7 @@ func (x *ListRoomsByOwnersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomsByOwnersRequest.ProtoReflect.Descriptor instead. func (*ListRoomsByOwnersRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{129} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{139} } func (x *ListRoomsByOwnersRequest) GetMeta() *RequestMeta { @@ -10413,7 +11320,7 @@ type ListRoomFeedsRequest struct { func (x *ListRoomFeedsRequest) Reset() { *x = ListRoomFeedsRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[130] + mi := &file_proto_room_v1_room_proto_msgTypes[140] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10425,7 +11332,7 @@ func (x *ListRoomFeedsRequest) String() string { func (*ListRoomFeedsRequest) ProtoMessage() {} func (x *ListRoomFeedsRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[130] + mi := &file_proto_room_v1_room_proto_msgTypes[140] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10438,7 +11345,7 @@ func (x *ListRoomFeedsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomFeedsRequest.ProtoReflect.Descriptor instead. func (*ListRoomFeedsRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{130} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{140} } func (x *ListRoomFeedsRequest) GetMeta() *RequestMeta { @@ -10510,7 +11417,7 @@ type ListRoomGiftLeaderboardRequest struct { func (x *ListRoomGiftLeaderboardRequest) Reset() { *x = ListRoomGiftLeaderboardRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[131] + mi := &file_proto_room_v1_room_proto_msgTypes[141] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10522,7 +11429,7 @@ func (x *ListRoomGiftLeaderboardRequest) String() string { func (*ListRoomGiftLeaderboardRequest) ProtoMessage() {} func (x *ListRoomGiftLeaderboardRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[131] + mi := &file_proto_room_v1_room_proto_msgTypes[141] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10535,7 +11442,7 @@ func (x *ListRoomGiftLeaderboardRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomGiftLeaderboardRequest.ProtoReflect.Descriptor instead. func (*ListRoomGiftLeaderboardRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{131} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{141} } func (x *ListRoomGiftLeaderboardRequest) GetMeta() *RequestMeta { @@ -10577,7 +11484,7 @@ type RoomFeedRelatedUser struct { func (x *RoomFeedRelatedUser) Reset() { *x = RoomFeedRelatedUser{} - mi := &file_proto_room_v1_room_proto_msgTypes[132] + mi := &file_proto_room_v1_room_proto_msgTypes[142] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10589,7 +11496,7 @@ func (x *RoomFeedRelatedUser) String() string { func (*RoomFeedRelatedUser) ProtoMessage() {} func (x *RoomFeedRelatedUser) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[132] + mi := &file_proto_room_v1_room_proto_msgTypes[142] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10602,7 +11509,7 @@ func (x *RoomFeedRelatedUser) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomFeedRelatedUser.ProtoReflect.Descriptor instead. func (*RoomFeedRelatedUser) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{132} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{142} } func (x *RoomFeedRelatedUser) GetUserId() int64 { @@ -10621,29 +11528,31 @@ func (x *RoomFeedRelatedUser) GetRelationUpdatedAtMs() int64 { // RoomListItem 是房间发现页卡片读模型,不承载完整 Room Cell 状态。 type RoomListItem struct { - state protoimpl.MessageState `protogen:"open.v1"` - RoomId string `protobuf:"bytes,1,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"` - OwnerUserId int64 `protobuf:"varint,2,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"` - Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"` - CoverUrl string `protobuf:"bytes,5,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"` - Mode string `protobuf:"bytes,6,opt,name=mode,proto3" json:"mode,omitempty"` - Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` - Heat int64 `protobuf:"varint,8,opt,name=heat,proto3" json:"heat,omitempty"` - OnlineCount int32 `protobuf:"varint,9,opt,name=online_count,json=onlineCount,proto3" json:"online_count,omitempty"` - SeatCount int32 `protobuf:"varint,10,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"` - OccupiedSeatCount int32 `protobuf:"varint,11,opt,name=occupied_seat_count,json=occupiedSeatCount,proto3" json:"occupied_seat_count,omitempty"` - VisibleRegionId int64 `protobuf:"varint,12,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"` - AppCode string `protobuf:"bytes,13,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` - RoomShortId string `protobuf:"bytes,14,opt,name=room_short_id,json=roomShortId,proto3" json:"room_short_id,omitempty"` - Locked bool `protobuf:"varint,15,opt,name=locked,proto3" json:"locked,omitempty"` - CountryCode string `protobuf:"bytes,16,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + RoomId string `protobuf:"bytes,1,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"` + OwnerUserId int64 `protobuf:"varint,2,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"` + Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"` + CoverUrl string `protobuf:"bytes,5,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"` + Mode string `protobuf:"bytes,6,opt,name=mode,proto3" json:"mode,omitempty"` + Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` + Heat int64 `protobuf:"varint,8,opt,name=heat,proto3" json:"heat,omitempty"` + OnlineCount int32 `protobuf:"varint,9,opt,name=online_count,json=onlineCount,proto3" json:"online_count,omitempty"` + SeatCount int32 `protobuf:"varint,10,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"` + OccupiedSeatCount int32 `protobuf:"varint,11,opt,name=occupied_seat_count,json=occupiedSeatCount,proto3" json:"occupied_seat_count,omitempty"` + VisibleRegionId int64 `protobuf:"varint,12,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"` + AppCode string `protobuf:"bytes,13,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` + RoomShortId string `protobuf:"bytes,14,opt,name=room_short_id,json=roomShortId,proto3" json:"room_short_id,omitempty"` + Locked bool `protobuf:"varint,15,opt,name=locked,proto3" json:"locked,omitempty"` + CountryCode string `protobuf:"bytes,16,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"` + RoomBorder *RoomDecorationResource `protobuf:"bytes,17,opt,name=room_border,json=roomBorder,proto3" json:"room_border,omitempty"` + RoomNameStyle *RoomDecorationResource `protobuf:"bytes,18,opt,name=room_name_style,json=roomNameStyle,proto3" json:"room_name_style,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *RoomListItem) Reset() { *x = RoomListItem{} - mi := &file_proto_room_v1_room_proto_msgTypes[133] + mi := &file_proto_room_v1_room_proto_msgTypes[143] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10655,7 +11564,7 @@ func (x *RoomListItem) String() string { func (*RoomListItem) ProtoMessage() {} func (x *RoomListItem) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[133] + mi := &file_proto_room_v1_room_proto_msgTypes[143] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10668,7 +11577,7 @@ func (x *RoomListItem) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomListItem.ProtoReflect.Descriptor instead. func (*RoomListItem) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{133} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{143} } func (x *RoomListItem) GetRoomId() string { @@ -10776,6 +11685,20 @@ func (x *RoomListItem) GetCountryCode() string { return "" } +func (x *RoomListItem) GetRoomBorder() *RoomDecorationResource { + if x != nil { + return x.RoomBorder + } + return nil +} + +func (x *RoomListItem) GetRoomNameStyle() *RoomDecorationResource { + if x != nil { + return x.RoomNameStyle + } + return nil +} + // ListRoomsResponse 返回一页房间卡片和下一页不透明 cursor。 type ListRoomsResponse struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -10787,7 +11710,7 @@ type ListRoomsResponse struct { func (x *ListRoomsResponse) Reset() { *x = ListRoomsResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[134] + mi := &file_proto_room_v1_room_proto_msgTypes[144] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10799,7 +11722,7 @@ func (x *ListRoomsResponse) String() string { func (*ListRoomsResponse) ProtoMessage() {} func (x *ListRoomsResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[134] + mi := &file_proto_room_v1_room_proto_msgTypes[144] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10812,7 +11735,7 @@ func (x *ListRoomsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomsResponse.ProtoReflect.Descriptor instead. func (*ListRoomsResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{134} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{144} } func (x *ListRoomsResponse) GetRooms() []*RoomListItem { @@ -10842,7 +11765,7 @@ type RoomGiftLeaderboardItem struct { func (x *RoomGiftLeaderboardItem) Reset() { *x = RoomGiftLeaderboardItem{} - mi := &file_proto_room_v1_room_proto_msgTypes[135] + mi := &file_proto_room_v1_room_proto_msgTypes[145] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10854,7 +11777,7 @@ func (x *RoomGiftLeaderboardItem) String() string { func (*RoomGiftLeaderboardItem) ProtoMessage() {} func (x *RoomGiftLeaderboardItem) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[135] + mi := &file_proto_room_v1_room_proto_msgTypes[145] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10867,7 +11790,7 @@ func (x *RoomGiftLeaderboardItem) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomGiftLeaderboardItem.ProtoReflect.Descriptor instead. func (*RoomGiftLeaderboardItem) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{135} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{145} } func (x *RoomGiftLeaderboardItem) GetRank() int64 { @@ -10913,7 +11836,7 @@ type ListRoomGiftLeaderboardResponse struct { func (x *ListRoomGiftLeaderboardResponse) Reset() { *x = ListRoomGiftLeaderboardResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[136] + mi := &file_proto_room_v1_room_proto_msgTypes[146] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -10925,7 +11848,7 @@ func (x *ListRoomGiftLeaderboardResponse) String() string { func (*ListRoomGiftLeaderboardResponse) ProtoMessage() {} func (x *ListRoomGiftLeaderboardResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[136] + mi := &file_proto_room_v1_room_proto_msgTypes[146] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -10938,7 +11861,7 @@ func (x *ListRoomGiftLeaderboardResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomGiftLeaderboardResponse.ProtoReflect.Descriptor instead. func (*ListRoomGiftLeaderboardResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{136} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{146} } func (x *ListRoomGiftLeaderboardResponse) GetItems() []*RoomGiftLeaderboardItem { @@ -10997,7 +11920,7 @@ type ListRoomContributionRankRequest struct { func (x *ListRoomContributionRankRequest) Reset() { *x = ListRoomContributionRankRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[137] + mi := &file_proto_room_v1_room_proto_msgTypes[147] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11009,7 +11932,7 @@ func (x *ListRoomContributionRankRequest) String() string { func (*ListRoomContributionRankRequest) ProtoMessage() {} func (x *ListRoomContributionRankRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[137] + mi := &file_proto_room_v1_room_proto_msgTypes[147] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11022,7 +11945,7 @@ func (x *ListRoomContributionRankRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomContributionRankRequest.ProtoReflect.Descriptor instead. func (*ListRoomContributionRankRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{137} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{147} } func (x *ListRoomContributionRankRequest) GetMeta() *RequestMeta { @@ -11076,7 +11999,7 @@ type ListRoomContributionRankResponse struct { func (x *ListRoomContributionRankResponse) Reset() { *x = ListRoomContributionRankResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[138] + mi := &file_proto_room_v1_room_proto_msgTypes[148] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11088,7 +12011,7 @@ func (x *ListRoomContributionRankResponse) String() string { func (*ListRoomContributionRankResponse) ProtoMessage() {} func (x *ListRoomContributionRankResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[138] + mi := &file_proto_room_v1_room_proto_msgTypes[148] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11101,7 +12024,7 @@ func (x *ListRoomContributionRankResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomContributionRankResponse.ProtoReflect.Descriptor instead. func (*ListRoomContributionRankResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{138} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{148} } func (x *ListRoomContributionRankResponse) GetRoomId() string { @@ -11165,7 +12088,7 @@ type GetMyRoomRequest struct { func (x *GetMyRoomRequest) Reset() { *x = GetMyRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[139] + mi := &file_proto_room_v1_room_proto_msgTypes[149] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11177,7 +12100,7 @@ func (x *GetMyRoomRequest) String() string { func (*GetMyRoomRequest) ProtoMessage() {} func (x *GetMyRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[139] + mi := &file_proto_room_v1_room_proto_msgTypes[149] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11190,7 +12113,7 @@ func (x *GetMyRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMyRoomRequest.ProtoReflect.Descriptor instead. func (*GetMyRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{139} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{149} } func (x *GetMyRoomRequest) GetMeta() *RequestMeta { @@ -11220,7 +12143,7 @@ type GetMyRoomResponse struct { func (x *GetMyRoomResponse) Reset() { *x = GetMyRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[140] + mi := &file_proto_room_v1_room_proto_msgTypes[150] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11232,7 +12155,7 @@ func (x *GetMyRoomResponse) String() string { func (*GetMyRoomResponse) ProtoMessage() {} func (x *GetMyRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[140] + mi := &file_proto_room_v1_room_proto_msgTypes[150] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11245,7 +12168,7 @@ func (x *GetMyRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetMyRoomResponse.ProtoReflect.Descriptor instead. func (*GetMyRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{140} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{150} } func (x *GetMyRoomResponse) GetHasRoom() bool { @@ -11281,7 +12204,7 @@ type GetCurrentRoomRequest struct { func (x *GetCurrentRoomRequest) Reset() { *x = GetCurrentRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[141] + mi := &file_proto_room_v1_room_proto_msgTypes[151] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11293,7 +12216,7 @@ func (x *GetCurrentRoomRequest) String() string { func (*GetCurrentRoomRequest) ProtoMessage() {} func (x *GetCurrentRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[141] + mi := &file_proto_room_v1_room_proto_msgTypes[151] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11306,7 +12229,7 @@ func (x *GetCurrentRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCurrentRoomRequest.ProtoReflect.Descriptor instead. func (*GetCurrentRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{141} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{151} } func (x *GetCurrentRoomRequest) GetMeta() *RequestMeta { @@ -11342,7 +12265,7 @@ type GetCurrentRoomResponse struct { func (x *GetCurrentRoomResponse) Reset() { *x = GetCurrentRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[142] + mi := &file_proto_room_v1_room_proto_msgTypes[152] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11354,7 +12277,7 @@ func (x *GetCurrentRoomResponse) String() string { func (*GetCurrentRoomResponse) ProtoMessage() {} func (x *GetCurrentRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[142] + mi := &file_proto_room_v1_room_proto_msgTypes[152] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11367,7 +12290,7 @@ func (x *GetCurrentRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetCurrentRoomResponse.ProtoReflect.Descriptor instead. func (*GetCurrentRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{142} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{152} } func (x *GetCurrentRoomResponse) GetHasCurrentRoom() bool { @@ -11433,6 +12356,333 @@ func (x *GetCurrentRoomResponse) GetServerTimeMs() int64 { return 0 } +// BatchGetUserVoiceRoomPresencesRequest 查询一组用户当前可向 viewer 展示的上麦入口。 +// user_ids 只能由 gateway 从外部查询参数解析;room-service 仍会去重并限制最多 100 个。 +type BatchGetUserVoiceRoomPresencesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + ViewerUserId int64 `protobuf:"varint,2,opt,name=viewer_user_id,json=viewerUserId,proto3" json:"viewer_user_id,omitempty"` + UserIds []int64 `protobuf:"varint,3,rep,packed,name=user_ids,json=userIds,proto3" json:"user_ids,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchGetUserVoiceRoomPresencesRequest) Reset() { + *x = BatchGetUserVoiceRoomPresencesRequest{} + mi := &file_proto_room_v1_room_proto_msgTypes[153] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchGetUserVoiceRoomPresencesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchGetUserVoiceRoomPresencesRequest) ProtoMessage() {} + +func (x *BatchGetUserVoiceRoomPresencesRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[153] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchGetUserVoiceRoomPresencesRequest.ProtoReflect.Descriptor instead. +func (*BatchGetUserVoiceRoomPresencesRequest) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{153} +} + +func (x *BatchGetUserVoiceRoomPresencesRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *BatchGetUserVoiceRoomPresencesRequest) GetViewerUserId() int64 { + if x != nil { + return x.ViewerUserId + } + return 0 +} + +func (x *BatchGetUserVoiceRoomPresencesRequest) GetUserIds() []int64 { + if x != nil { + return x.UserIds + } + return nil +} + +// UserVoiceRoomPresenceRoom 是个人资料页 Live 入口需要的最小房间投影。 +// 它不携带在线用户和完整麦位集合,避免 batch 查询把 RoomSnapshot 明细扩散到用户域接口。 +type UserVoiceRoomPresenceRoom struct { + state protoimpl.MessageState `protogen:"open.v1"` + RoomId string `protobuf:"bytes,1,opt,name=room_id,json=roomId,proto3" json:"room_id,omitempty"` + RoomShortId string `protobuf:"bytes,2,opt,name=room_short_id,json=roomShortId,proto3" json:"room_short_id,omitempty"` + OwnerUserId int64 `protobuf:"varint,3,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"` + // 当前房间模型没有独立主持人字段,host_user_id 与 owner_user_id 保持一致。 + HostUserId int64 `protobuf:"varint,4,opt,name=host_user_id,json=hostUserId,proto3" json:"host_user_id,omitempty"` + Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"` + CoverUrl string `protobuf:"bytes,6,opt,name=cover_url,json=coverUrl,proto3" json:"cover_url,omitempty"` + BackgroundUrl string `protobuf:"bytes,7,opt,name=background_url,json=backgroundUrl,proto3" json:"background_url,omitempty"` + Mode string `protobuf:"bytes,8,opt,name=mode,proto3" json:"mode,omitempty"` + OnlineCount int32 `protobuf:"varint,9,opt,name=online_count,json=onlineCount,proto3" json:"online_count,omitempty"` + SeatCount int32 `protobuf:"varint,10,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"` + Locked bool `protobuf:"varint,11,opt,name=locked,proto3" json:"locked,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserVoiceRoomPresenceRoom) Reset() { + *x = UserVoiceRoomPresenceRoom{} + mi := &file_proto_room_v1_room_proto_msgTypes[154] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserVoiceRoomPresenceRoom) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserVoiceRoomPresenceRoom) ProtoMessage() {} + +func (x *UserVoiceRoomPresenceRoom) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[154] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserVoiceRoomPresenceRoom.ProtoReflect.Descriptor instead. +func (*UserVoiceRoomPresenceRoom) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{154} +} + +func (x *UserVoiceRoomPresenceRoom) GetRoomId() string { + if x != nil { + return x.RoomId + } + return "" +} + +func (x *UserVoiceRoomPresenceRoom) GetRoomShortId() string { + if x != nil { + return x.RoomShortId + } + return "" +} + +func (x *UserVoiceRoomPresenceRoom) GetOwnerUserId() int64 { + if x != nil { + return x.OwnerUserId + } + return 0 +} + +func (x *UserVoiceRoomPresenceRoom) GetHostUserId() int64 { + if x != nil { + return x.HostUserId + } + return 0 +} + +func (x *UserVoiceRoomPresenceRoom) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *UserVoiceRoomPresenceRoom) GetCoverUrl() string { + if x != nil { + return x.CoverUrl + } + return "" +} + +func (x *UserVoiceRoomPresenceRoom) GetBackgroundUrl() string { + if x != nil { + return x.BackgroundUrl + } + return "" +} + +func (x *UserVoiceRoomPresenceRoom) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *UserVoiceRoomPresenceRoom) GetOnlineCount() int32 { + if x != nil { + return x.OnlineCount + } + return 0 +} + +func (x *UserVoiceRoomPresenceRoom) GetSeatCount() int32 { + if x != nil { + return x.SeatCount + } + return 0 +} + +func (x *UserVoiceRoomPresenceRoom) GetLocked() bool { + if x != nil { + return x.Locked + } + return false +} + +// UserVoiceRoomPresence 对每个去重后的请求用户返回一条稳定结果。 +// 不可见、不可进入和未上麦统一返回 not_on_mic,避免通过差异状态探测房间位置。 +type UserVoiceRoomPresence struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + SeatNo int32 `protobuf:"varint,3,opt,name=seat_no,json=seatNo,proto3" json:"seat_no,omitempty"` + Joinable bool `protobuf:"varint,4,opt,name=joinable,proto3" json:"joinable,omitempty"` + ObservedAtMs int64 `protobuf:"varint,5,opt,name=observed_at_ms,json=observedAtMs,proto3" json:"observed_at_ms,omitempty"` + Room *UserVoiceRoomPresenceRoom `protobuf:"bytes,6,opt,name=room,proto3" json:"room,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserVoiceRoomPresence) Reset() { + *x = UserVoiceRoomPresence{} + mi := &file_proto_room_v1_room_proto_msgTypes[155] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserVoiceRoomPresence) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserVoiceRoomPresence) ProtoMessage() {} + +func (x *UserVoiceRoomPresence) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[155] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserVoiceRoomPresence.ProtoReflect.Descriptor instead. +func (*UserVoiceRoomPresence) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{155} +} + +func (x *UserVoiceRoomPresence) GetUserId() int64 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *UserVoiceRoomPresence) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *UserVoiceRoomPresence) GetSeatNo() int32 { + if x != nil { + return x.SeatNo + } + return 0 +} + +func (x *UserVoiceRoomPresence) GetJoinable() bool { + if x != nil { + return x.Joinable + } + return false +} + +func (x *UserVoiceRoomPresence) GetObservedAtMs() int64 { + if x != nil { + return x.ObservedAtMs + } + return 0 +} + +func (x *UserVoiceRoomPresence) GetRoom() *UserVoiceRoomPresenceRoom { + if x != nil { + return x.Room + } + return nil +} + +type BatchGetUserVoiceRoomPresencesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*UserVoiceRoomPresence `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + ServerTimeMs int64 `protobuf:"varint,2,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BatchGetUserVoiceRoomPresencesResponse) Reset() { + *x = BatchGetUserVoiceRoomPresencesResponse{} + mi := &file_proto_room_v1_room_proto_msgTypes[156] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BatchGetUserVoiceRoomPresencesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BatchGetUserVoiceRoomPresencesResponse) ProtoMessage() {} + +func (x *BatchGetUserVoiceRoomPresencesResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_room_v1_room_proto_msgTypes[156] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BatchGetUserVoiceRoomPresencesResponse.ProtoReflect.Descriptor instead. +func (*BatchGetUserVoiceRoomPresencesResponse) Descriptor() ([]byte, []int) { + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{156} +} + +func (x *BatchGetUserVoiceRoomPresencesResponse) GetItems() []*UserVoiceRoomPresence { + if x != nil { + return x.Items + } + return nil +} + +func (x *BatchGetUserVoiceRoomPresencesResponse) GetServerTimeMs() int64 { + if x != nil { + return x.ServerTimeMs + } + return 0 +} + // GetRoomSnapshotRequest 主动读取当前房间完整快照。 // 该查询必须由 gateway 写入鉴权后的 viewer_user_id,客户端不能提交 viewer_user_id。 type GetRoomSnapshotRequest struct { @@ -11446,7 +12696,7 @@ type GetRoomSnapshotRequest struct { func (x *GetRoomSnapshotRequest) Reset() { *x = GetRoomSnapshotRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[143] + mi := &file_proto_room_v1_room_proto_msgTypes[157] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11458,7 +12708,7 @@ func (x *GetRoomSnapshotRequest) String() string { func (*GetRoomSnapshotRequest) ProtoMessage() {} func (x *GetRoomSnapshotRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[143] + mi := &file_proto_room_v1_room_proto_msgTypes[157] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11471,7 +12721,7 @@ func (x *GetRoomSnapshotRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRoomSnapshotRequest.ProtoReflect.Descriptor instead. func (*GetRoomSnapshotRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{143} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{157} } func (x *GetRoomSnapshotRequest) GetMeta() *RequestMeta { @@ -11508,7 +12758,7 @@ type GetRoomSnapshotResponse struct { func (x *GetRoomSnapshotResponse) Reset() { *x = GetRoomSnapshotResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[144] + mi := &file_proto_room_v1_room_proto_msgTypes[158] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11520,7 +12770,7 @@ func (x *GetRoomSnapshotResponse) String() string { func (*GetRoomSnapshotResponse) ProtoMessage() {} func (x *GetRoomSnapshotResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[144] + mi := &file_proto_room_v1_room_proto_msgTypes[158] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11533,7 +12783,7 @@ func (x *GetRoomSnapshotResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRoomSnapshotResponse.ProtoReflect.Descriptor instead. func (*GetRoomSnapshotResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{144} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{158} } func (x *GetRoomSnapshotResponse) GetRoom() *RoomSnapshot { @@ -11570,7 +12820,7 @@ type GetRoomRocketRequest struct { func (x *GetRoomRocketRequest) Reset() { *x = GetRoomRocketRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[145] + mi := &file_proto_room_v1_room_proto_msgTypes[159] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11582,7 +12832,7 @@ func (x *GetRoomRocketRequest) String() string { func (*GetRoomRocketRequest) ProtoMessage() {} func (x *GetRoomRocketRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[145] + mi := &file_proto_room_v1_room_proto_msgTypes[159] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11595,7 +12845,7 @@ func (x *GetRoomRocketRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRoomRocketRequest.ProtoReflect.Descriptor instead. func (*GetRoomRocketRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{145} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{159} } func (x *GetRoomRocketRequest) GetMeta() *RequestMeta { @@ -11629,7 +12879,7 @@ type GetRoomRocketResponse struct { func (x *GetRoomRocketResponse) Reset() { *x = GetRoomRocketResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[146] + mi := &file_proto_room_v1_room_proto_msgTypes[160] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11641,7 +12891,7 @@ func (x *GetRoomRocketResponse) String() string { func (*GetRoomRocketResponse) ProtoMessage() {} func (x *GetRoomRocketResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[146] + mi := &file_proto_room_v1_room_proto_msgTypes[160] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11654,7 +12904,7 @@ func (x *GetRoomRocketResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use GetRoomRocketResponse.ProtoReflect.Descriptor instead. func (*GetRoomRocketResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{146} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{160} } func (x *GetRoomRocketResponse) GetRocket() *RoomRocketInfo { @@ -11686,7 +12936,7 @@ type ListRoomOnlineUsersRequest struct { func (x *ListRoomOnlineUsersRequest) Reset() { *x = ListRoomOnlineUsersRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[147] + mi := &file_proto_room_v1_room_proto_msgTypes[161] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11698,7 +12948,7 @@ func (x *ListRoomOnlineUsersRequest) String() string { func (*ListRoomOnlineUsersRequest) ProtoMessage() {} func (x *ListRoomOnlineUsersRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[147] + mi := &file_proto_room_v1_room_proto_msgTypes[161] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11711,7 +12961,7 @@ func (x *ListRoomOnlineUsersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomOnlineUsersRequest.ProtoReflect.Descriptor instead. func (*ListRoomOnlineUsersRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{147} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{161} } func (x *ListRoomOnlineUsersRequest) GetMeta() *RequestMeta { @@ -11770,7 +13020,7 @@ type ListRoomOnlineUsersResponse struct { func (x *ListRoomOnlineUsersResponse) Reset() { *x = ListRoomOnlineUsersResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[148] + mi := &file_proto_room_v1_room_proto_msgTypes[162] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11782,7 +13032,7 @@ func (x *ListRoomOnlineUsersResponse) String() string { func (*ListRoomOnlineUsersResponse) ProtoMessage() {} func (x *ListRoomOnlineUsersResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[148] + mi := &file_proto_room_v1_room_proto_msgTypes[162] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11795,7 +13045,7 @@ func (x *ListRoomOnlineUsersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomOnlineUsersResponse.ProtoReflect.Descriptor instead. func (*ListRoomOnlineUsersResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{148} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{162} } func (x *ListRoomOnlineUsersResponse) GetUsers() []*RoomUser { @@ -11856,7 +13106,7 @@ type RoomBannedUser struct { func (x *RoomBannedUser) Reset() { *x = RoomBannedUser{} - mi := &file_proto_room_v1_room_proto_msgTypes[149] + mi := &file_proto_room_v1_room_proto_msgTypes[163] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11868,7 +13118,7 @@ func (x *RoomBannedUser) String() string { func (*RoomBannedUser) ProtoMessage() {} func (x *RoomBannedUser) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[149] + mi := &file_proto_room_v1_room_proto_msgTypes[163] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11881,7 +13131,7 @@ func (x *RoomBannedUser) ProtoReflect() protoreflect.Message { // Deprecated: Use RoomBannedUser.ProtoReflect.Descriptor instead. func (*RoomBannedUser) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{149} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{163} } func (x *RoomBannedUser) GetUserId() int64 { @@ -11940,7 +13190,7 @@ type ListRoomBannedUsersRequest struct { func (x *ListRoomBannedUsersRequest) Reset() { *x = ListRoomBannedUsersRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[150] + mi := &file_proto_room_v1_room_proto_msgTypes[164] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -11952,7 +13202,7 @@ func (x *ListRoomBannedUsersRequest) String() string { func (*ListRoomBannedUsersRequest) ProtoMessage() {} func (x *ListRoomBannedUsersRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[150] + mi := &file_proto_room_v1_room_proto_msgTypes[164] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -11965,7 +13215,7 @@ func (x *ListRoomBannedUsersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomBannedUsersRequest.ProtoReflect.Descriptor instead. func (*ListRoomBannedUsersRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{150} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{164} } func (x *ListRoomBannedUsersRequest) GetMeta() *RequestMeta { @@ -12016,7 +13266,7 @@ type ListRoomBannedUsersResponse struct { func (x *ListRoomBannedUsersResponse) Reset() { *x = ListRoomBannedUsersResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[151] + mi := &file_proto_room_v1_room_proto_msgTypes[165] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12028,7 +13278,7 @@ func (x *ListRoomBannedUsersResponse) String() string { func (*ListRoomBannedUsersResponse) ProtoMessage() {} func (x *ListRoomBannedUsersResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[151] + mi := &file_proto_room_v1_room_proto_msgTypes[165] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12041,7 +13291,7 @@ func (x *ListRoomBannedUsersResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListRoomBannedUsersResponse.ProtoReflect.Descriptor instead. func (*ListRoomBannedUsersResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{151} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{165} } func (x *ListRoomBannedUsersResponse) GetItems() []*RoomBannedUser { @@ -12092,7 +13342,7 @@ type FollowRoomRequest struct { func (x *FollowRoomRequest) Reset() { *x = FollowRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[152] + mi := &file_proto_room_v1_room_proto_msgTypes[166] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12104,7 +13354,7 @@ func (x *FollowRoomRequest) String() string { func (*FollowRoomRequest) ProtoMessage() {} func (x *FollowRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[152] + mi := &file_proto_room_v1_room_proto_msgTypes[166] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12117,7 +13367,7 @@ func (x *FollowRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use FollowRoomRequest.ProtoReflect.Descriptor instead. func (*FollowRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{152} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{166} } func (x *FollowRoomRequest) GetMeta() *RequestMeta { @@ -12153,7 +13403,7 @@ type FollowRoomResponse struct { func (x *FollowRoomResponse) Reset() { *x = FollowRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[153] + mi := &file_proto_room_v1_room_proto_msgTypes[167] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12165,7 +13415,7 @@ func (x *FollowRoomResponse) String() string { func (*FollowRoomResponse) ProtoMessage() {} func (x *FollowRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[153] + mi := &file_proto_room_v1_room_proto_msgTypes[167] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12178,7 +13428,7 @@ func (x *FollowRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use FollowRoomResponse.ProtoReflect.Descriptor instead. func (*FollowRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{153} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{167} } func (x *FollowRoomResponse) GetRoomId() string { @@ -12221,7 +13471,7 @@ type UnfollowRoomRequest struct { func (x *UnfollowRoomRequest) Reset() { *x = UnfollowRoomRequest{} - mi := &file_proto_room_v1_room_proto_msgTypes[154] + mi := &file_proto_room_v1_room_proto_msgTypes[168] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12233,7 +13483,7 @@ func (x *UnfollowRoomRequest) String() string { func (*UnfollowRoomRequest) ProtoMessage() {} func (x *UnfollowRoomRequest) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[154] + mi := &file_proto_room_v1_room_proto_msgTypes[168] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12246,7 +13496,7 @@ func (x *UnfollowRoomRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UnfollowRoomRequest.ProtoReflect.Descriptor instead. func (*UnfollowRoomRequest) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{154} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{168} } func (x *UnfollowRoomRequest) GetMeta() *RequestMeta { @@ -12281,7 +13531,7 @@ type UnfollowRoomResponse struct { func (x *UnfollowRoomResponse) Reset() { *x = UnfollowRoomResponse{} - mi := &file_proto_room_v1_room_proto_msgTypes[155] + mi := &file_proto_room_v1_room_proto_msgTypes[169] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12293,7 +13543,7 @@ func (x *UnfollowRoomResponse) String() string { func (*UnfollowRoomResponse) ProtoMessage() {} func (x *UnfollowRoomResponse) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[155] + mi := &file_proto_room_v1_room_proto_msgTypes[169] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12306,7 +13556,7 @@ func (x *UnfollowRoomResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use UnfollowRoomResponse.ProtoReflect.Descriptor instead. func (*UnfollowRoomResponse) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{155} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{169} } func (x *UnfollowRoomResponse) GetRoomId() string { @@ -12346,7 +13596,7 @@ type SendGiftLuckyHit struct { func (x *SendGiftLuckyHit) Reset() { *x = SendGiftLuckyHit{} - mi := &file_proto_room_v1_room_proto_msgTypes[156] + mi := &file_proto_room_v1_room_proto_msgTypes[170] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12358,7 +13608,7 @@ func (x *SendGiftLuckyHit) String() string { func (*SendGiftLuckyHit) ProtoMessage() {} func (x *SendGiftLuckyHit) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[156] + mi := &file_proto_room_v1_room_proto_msgTypes[170] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12371,7 +13621,7 @@ func (x *SendGiftLuckyHit) ProtoReflect() protoreflect.Message { // Deprecated: Use SendGiftLuckyHit.ProtoReflect.Descriptor instead. func (*SendGiftLuckyHit) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{156} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{170} } func (x *SendGiftLuckyHit) GetGiftIndex() int32 { @@ -12448,7 +13698,7 @@ type SendGiftResultV2 struct { func (x *SendGiftResultV2) Reset() { *x = SendGiftResultV2{} - mi := &file_proto_room_v1_room_proto_msgTypes[157] + mi := &file_proto_room_v1_room_proto_msgTypes[171] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -12460,7 +13710,7 @@ func (x *SendGiftResultV2) String() string { func (*SendGiftResultV2) ProtoMessage() {} func (x *SendGiftResultV2) ProtoReflect() protoreflect.Message { - mi := &file_proto_room_v1_room_proto_msgTypes[157] + mi := &file_proto_room_v1_room_proto_msgTypes[171] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -12473,7 +13723,7 @@ func (x *SendGiftResultV2) ProtoReflect() protoreflect.Message { // Deprecated: Use SendGiftResultV2.ProtoReflect.Descriptor instead. func (*SendGiftResultV2) Descriptor() ([]byte, []int) { - return file_proto_room_v1_room_proto_rawDescGZIP(), []int{157} + return file_proto_room_v1_room_proto_rawDescGZIP(), []int{171} } func (x *SendGiftResultV2) GetApiVersion() int32 { @@ -13022,7 +14272,7 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\"\n" + "\ractor_user_id\x18\x02 \x01(\x03R\vactorUserId\x12\"\n" + "\rcreated_at_ms\x18\x03 \x01(\x03R\vcreatedAtMs\x12\"\n" + - "\rexpires_at_ms\x18\x04 \x01(\x03R\vexpiresAtMs\"\xf8\x06\n" + + "\rexpires_at_ms\x18\x04 \x01(\x03R\vexpiresAtMs\"\xe0\b\n" + "\fRoomSnapshot\x12\x17\n" + "\aroom_id\x18\x01 \x01(\tR\x06roomId\x12\"\n" + "\rowner_user_id\x18\x02 \x01(\x03R\vownerUserId\x12\x12\n" + @@ -13047,21 +14297,50 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\x06rocket\x18\x14 \x01(\v2\x1e.hyapp.room.v1.RoomRocketStateR\x06rocket\x12:\n" + "\n" + "ban_states\x18\x15 \x03(\v2\x1b.hyapp.room.v1.RoomBanStateR\tbanStates\x12!\n" + - "\fonline_count\x18\x16 \x01(\x05R\vonlineCount\x1a:\n" + + "\fonline_count\x18\x16 \x01(\x05R\vonlineCount\x12F\n" + + "\vroom_border\x18\x17 \x01(\v2%.hyapp.room.v1.RoomDecorationResourceR\n" + + "roomBorder\x12M\n" + + "\x0froom_name_style\x18\x18 \x01(\v2%.hyapp.room.v1.RoomDecorationResourceR\rroomNameStyle\x12O\n" + + "\x11active_background\x18\x19 \x01(\v2\".hyapp.room.v1.RoomBackgroundImageR\x10activeBackground\x1a:\n" + "\fRoomExtEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe5\x01\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\x96\x03\n" + + "\tRoomMedia\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12\x1d\n" + + "\n" + + "object_key\x18\x02 \x01(\tR\tobjectKey\x12!\n" + + "\fcontent_type\x18\x03 \x01(\tR\vcontentType\x12\x16\n" + + "\x06format\x18\x04 \x01(\tR\x06format\x12\x1d\n" + + "\n" + + "size_bytes\x18\x05 \x01(\x03R\tsizeBytes\x12\x14\n" + + "\x05width\x18\x06 \x01(\x05R\x05width\x12\x16\n" + + "\x06height\x18\a \x01(\x05R\x06height\x12\x1a\n" + + "\banimated\x18\b \x01(\bR\banimated\x12\x1f\n" + + "\vframe_count\x18\t \x01(\x05R\n" + + "frameCount\x12\x1f\n" + + "\vduration_ms\x18\n" + + " \x01(\x03R\n" + + "durationMs\x12\x1d\n" + + "\n" + + "poster_url\x18\v \x01(\tR\tposterUrl\x12#\n" + + "\rthumbnail_url\x18\f \x01(\tR\fthumbnailUrl\x12\x16\n" + + "\x06sha256\x18\r \x01(\tR\x06sha256\x12\x16\n" + + "\x06status\x18\x0e \x01(\tR\x06status\"\xb4\x02\n" + "\x13RoomBackgroundImage\x12#\n" + "\rbackground_id\x18\x01 \x01(\x03R\fbackgroundId\x12\x17\n" + "\aroom_id\x18\x02 \x01(\tR\x06roomId\x12\x1b\n" + "\timage_url\x18\x03 \x01(\tR\bimageUrl\x12+\n" + "\x12created_by_user_id\x18\x04 \x01(\x03R\x0fcreatedByUserId\x12\"\n" + "\rcreated_at_ms\x18\x05 \x01(\x03R\vcreatedAtMs\x12\"\n" + - "\rupdated_at_ms\x18\x06 \x01(\x03R\vupdatedAtMs\"\x81\x01\n" + + "\rupdated_at_ms\x18\x06 \x01(\x03R\vupdatedAtMs\x12.\n" + + "\x05media\x18\a \x01(\v2\x18.hyapp.room.v1.RoomMediaR\x05media\x12\x1d\n" + + "\n" + + "command_id\x18\b \x01(\tR\tcommandId\"\xb1\x01\n" + "\x19SaveRoomBackgroundRequest\x12.\n" + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12\x17\n" + "\aroom_id\x18\x02 \x01(\tR\x06roomId\x12\x1b\n" + - "\timage_url\x18\x03 \x01(\tR\bimageUrl\"\x86\x01\n" + + "\timage_url\x18\x03 \x01(\tR\bimageUrl\x12.\n" + + "\x05media\x18\x04 \x01(\v2\x18.hyapp.room.v1.RoomMediaR\x05media\"\x86\x01\n" + "\x1aSaveRoomBackgroundResponse\x12B\n" + "\n" + "background\x18\x01 \x01(\v2\".hyapp.room.v1.RoomBackgroundImageR\n" + @@ -13070,11 +14349,12 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\x1aListRoomBackgroundsRequest\x12.\n" + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12\x17\n" + "\aroom_id\x18\x02 \x01(\tR\x06roomId\x12$\n" + - "\x0eviewer_user_id\x18\x03 \x01(\x03R\fviewerUserId\"\xc1\x01\n" + + "\x0eviewer_user_id\x18\x03 \x01(\x03R\fviewerUserId\"\x96\x02\n" + "\x1bListRoomBackgroundsResponse\x12D\n" + "\vbackgrounds\x18\x01 \x03(\v2\".hyapp.room.v1.RoomBackgroundImageR\vbackgrounds\x126\n" + "\x17selected_background_url\x18\x02 \x01(\tR\x15selectedBackgroundUrl\x12$\n" + - "\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs\"o\n" + + "\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs\x12S\n" + + "\x13selected_background\x18\x04 \x01(\v2\".hyapp.room.v1.RoomBackgroundImageR\x12selectedBackground\"o\n" + "\x18SetRoomBackgroundRequest\x12.\n" + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12#\n" + "\rbackground_id\x18\x02 \x01(\x03R\fbackgroundId\"\xc6\x01\n" + @@ -13083,7 +14363,69 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\x04room\x18\x02 \x01(\v2\x1b.hyapp.room.v1.RoomSnapshotR\x04room\x12B\n" + "\n" + "background\x18\x03 \x01(\v2\".hyapp.room.v1.RoomBackgroundImageR\n" + - "background\"\xa2\x03\n" + + "background\"\xde\x03\n" + + "\x16RoomDecorationResource\x12\x1f\n" + + "\vresource_id\x18\x01 \x01(\x03R\n" + + "resourceId\x12#\n" + + "\rresource_code\x18\x02 \x01(\tR\fresourceCode\x12#\n" + + "\rresource_type\x18\x03 \x01(\tR\fresourceType\x12\x12\n" + + "\x04name\x18\x04 \x01(\tR\x04name\x12\x1b\n" + + "\tasset_url\x18\x05 \x01(\tR\bassetUrl\x12\x1f\n" + + "\vpreview_url\x18\x06 \x01(\tR\n" + + "previewUrl\x12#\n" + + "\ranimation_url\x18\a \x01(\tR\fanimationUrl\x12\x16\n" + + "\x06format\x18\b \x01(\tR\x06format\x12#\n" + + "\rmetadata_json\x18\t \x01(\tR\fmetadataJson\x12%\n" + + "\x0eentitlement_id\x18\n" + + " \x01(\tR\rentitlementId\x12\x1f\n" + + "\vtext_colors\x18\v \x03(\tR\n" + + "textColors\x12\x14\n" + + "\x05stops\x18\f \x03(\x01R\x05stops\x12#\n" + + "\rangle_degrees\x18\r \x01(\x01R\fangleDegrees\x12\"\n" + + "\rexpires_at_ms\x18\x0e \x01(\x03R\vexpiresAtMs\"\xbd\x01\n" + + "\x1aApplyRoomDecorationRequest\x12.\n" + + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12'\n" + + "\x0fdecoration_type\x18\x02 \x01(\tR\x0edecorationType\x12\x1f\n" + + "\vresource_id\x18\x03 \x01(\x03R\n" + + "resourceId\x12%\n" + + "\x0eentitlement_id\x18\x04 \x01(\tR\rentitlementId\"\x96\x02\n" + + "\x1bApplyRoomDecorationResponse\x124\n" + + "\x06result\x18\x01 \x01(\v2\x1c.hyapp.room.v1.CommandResultR\x06result\x12/\n" + + "\x04room\x18\x02 \x01(\v2\x1b.hyapp.room.v1.RoomSnapshotR\x04room\x12'\n" + + "\x0fdecoration_type\x18\x03 \x01(\tR\x0edecorationType\x12A\n" + + "\bresource\x18\x04 \x01(\v2%.hyapp.room.v1.RoomDecorationResourceR\bresource\x12$\n" + + "\x0eserver_time_ms\x18\x05 \x01(\x03R\fserverTimeMs\"\x9b\x01\n" + + "\x1fAuthorizeRoomMediaUploadRequest\x12.\n" + + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12\x18\n" + + "\apurpose\x18\x02 \x01(\tR\apurpose\x12.\n" + + "\x05media\x18\x03 \x01(\v2\x18.hyapp.room.v1.RoomMediaR\x05media\"\xd0\x01\n" + + " AuthorizeRoomMediaUploadResponse\x12\x18\n" + + "\aallowed\x18\x01 \x01(\bR\aallowed\x12*\n" + + "\x11object_key_prefix\x18\x02 \x01(\tR\x0fobjectKeyPrefix\x12!\n" + + "\froom_version\x18\x03 \x01(\x03R\vroomVersion\x12$\n" + + "\x0eserver_time_ms\x18\x04 \x01(\x03R\fserverTimeMs\x12\x1d\n" + + "\n" + + "object_key\x18\x05 \x01(\tR\tobjectKey\"\x9a\x01\n" + + "\x1eCompleteRoomMediaUploadRequest\x12.\n" + + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12\x18\n" + + "\apurpose\x18\x02 \x01(\tR\apurpose\x12.\n" + + "\x05media\x18\x03 \x01(\v2\x18.hyapp.room.v1.RoomMediaR\x05media\"\x8f\x01\n" + + "\x1fCompleteRoomMediaUploadResponse\x12\x16\n" + + "\x06active\x18\x01 \x01(\bR\x06active\x12.\n" + + "\x05media\x18\x02 \x01(\v2\x18.hyapp.room.v1.RoomMediaR\x05media\x12$\n" + + "\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs\"v\n" + + "\x14SendRoomImageRequest\x12.\n" + + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12.\n" + + "\x05image\x18\x02 \x01(\v2\x18.hyapp.room.v1.RoomMediaR\x05image\"\x9d\x02\n" + + "\x15SendRoomImageResponse\x124\n" + + "\x06result\x18\x01 \x01(\v2\x1c.hyapp.room.v1.CommandResultR\x06result\x12\x1a\n" + + "\baccepted\x18\x02 \x01(\bR\baccepted\x12\x1d\n" + + "\n" + + "message_id\x18\x03 \x01(\tR\tmessageId\x12\x17\n" + + "\aroom_id\x18\x04 \x01(\tR\x06roomId\x12$\n" + + "\x0esender_user_id\x18\x05 \x01(\x03R\fsenderUserId\x12$\n" + + "\x0eserver_time_ms\x18\x06 \x01(\x03R\fserverTimeMs\x12.\n" + + "\x05image\x18\a \x01(\v2\x18.hyapp.room.v1.RoomMediaR\x05image\"\xa2\x03\n" + "\x11CreateRoomRequest\x12.\n" + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12\x1d\n" + "\n" + @@ -13547,7 +14889,7 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\tpage_size\x18\x04 \x01(\x05R\bpageSize\"c\n" + "\x13RoomFeedRelatedUser\x12\x17\n" + "\auser_id\x18\x01 \x01(\x03R\x06userId\x123\n" + - "\x16relation_updated_at_ms\x18\x02 \x01(\x03R\x13relationUpdatedAtMs\"\xd6\x03\n" + + "\x16relation_updated_at_ms\x18\x02 \x01(\x03R\x13relationUpdatedAtMs\"\xed\x04\n" + "\fRoomListItem\x12\x17\n" + "\aroom_id\x18\x01 \x01(\tR\x06roomId\x12\"\n" + "\rowner_user_id\x18\x02 \x01(\x03R\vownerUserId\x12\x14\n" + @@ -13565,7 +14907,10 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\bapp_code\x18\r \x01(\tR\aappCode\x12\"\n" + "\rroom_short_id\x18\x0e \x01(\tR\vroomShortId\x12\x16\n" + "\x06locked\x18\x0f \x01(\bR\x06locked\x12!\n" + - "\fcountry_code\x18\x10 \x01(\tR\vcountryCode\"g\n" + + "\fcountry_code\x18\x10 \x01(\tR\vcountryCode\x12F\n" + + "\vroom_border\x18\x11 \x01(\v2%.hyapp.room.v1.RoomDecorationResourceR\n" + + "roomBorder\x12M\n" + + "\x0froom_name_style\x18\x12 \x01(\v2%.hyapp.room.v1.RoomDecorationResourceR\rroomNameStyle\"g\n" + "\x11ListRoomsResponse\x121\n" + "\x05rooms\x18\x01 \x03(\v2\x1b.hyapp.room.v1.RoomListItemR\x05rooms\x12\x1f\n" + "\vnext_cursor\x18\x02 \x01(\tR\n" + @@ -13616,7 +14961,36 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\rpublish_state\x18\x06 \x01(\tR\fpublishState\x12+\n" + "\x12need_join_im_group\x18\a \x01(\bR\x0fneedJoinImGroup\x12$\n" + "\x0eneed_rtc_token\x18\b \x01(\bR\fneedRtcToken\x12$\n" + - "\x0eserver_time_ms\x18\t \x01(\x03R\fserverTimeMs\"\x87\x01\n" + + "\x0eserver_time_ms\x18\t \x01(\x03R\fserverTimeMs\"\x98\x01\n" + + "%BatchGetUserVoiceRoomPresencesRequest\x12.\n" + + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12$\n" + + "\x0eviewer_user_id\x18\x02 \x01(\x03R\fviewerUserId\x12\x19\n" + + "\buser_ids\x18\x03 \x03(\x03R\auserIds\"\xe6\x02\n" + + "\x19UserVoiceRoomPresenceRoom\x12\x17\n" + + "\aroom_id\x18\x01 \x01(\tR\x06roomId\x12\"\n" + + "\rroom_short_id\x18\x02 \x01(\tR\vroomShortId\x12\"\n" + + "\rowner_user_id\x18\x03 \x01(\x03R\vownerUserId\x12 \n" + + "\fhost_user_id\x18\x04 \x01(\x03R\n" + + "hostUserId\x12\x14\n" + + "\x05title\x18\x05 \x01(\tR\x05title\x12\x1b\n" + + "\tcover_url\x18\x06 \x01(\tR\bcoverUrl\x12%\n" + + "\x0ebackground_url\x18\a \x01(\tR\rbackgroundUrl\x12\x12\n" + + "\x04mode\x18\b \x01(\tR\x04mode\x12!\n" + + "\fonline_count\x18\t \x01(\x05R\vonlineCount\x12\x1d\n" + + "\n" + + "seat_count\x18\n" + + " \x01(\x05R\tseatCount\x12\x16\n" + + "\x06locked\x18\v \x01(\bR\x06locked\"\xdf\x01\n" + + "\x15UserVoiceRoomPresence\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x14\n" + + "\x05state\x18\x02 \x01(\tR\x05state\x12\x17\n" + + "\aseat_no\x18\x03 \x01(\x05R\x06seatNo\x12\x1a\n" + + "\bjoinable\x18\x04 \x01(\bR\bjoinable\x12$\n" + + "\x0eobserved_at_ms\x18\x05 \x01(\x03R\fobservedAtMs\x12<\n" + + "\x04room\x18\x06 \x01(\v2(.hyapp.room.v1.UserVoiceRoomPresenceRoomR\x04room\"\x8a\x01\n" + + "&BatchGetUserVoiceRoomPresencesResponse\x12:\n" + + "\x05items\x18\x01 \x03(\v2$.hyapp.room.v1.UserVoiceRoomPresenceR\x05items\x12$\n" + + "\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\x87\x01\n" + "\x16GetRoomSnapshotRequest\x12.\n" + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12\x17\n" + "\aroom_id\x18\x02 \x01(\tR\x06roomId\x12$\n" + @@ -13716,13 +15090,15 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\rbatch_display\x18\x10 \x01(\v2#.hyapp.room.v1.SendGiftBatchDisplayR\fbatchDisplay\x12\x1b\n" + "\troom_heat\x18\x11 \x01(\x03R\broomHeat\x124\n" + "\tgift_rank\x18\x12 \x03(\v2\x17.hyapp.room.v1.RankItemR\bgiftRank\x12)\n" + - "\x10operation_status\x18\x13 \x01(\tR\x0foperationStatus2\xb2\x1b\n" + + "\x10operation_status\x18\x13 \x01(\tR\x0foperationStatus2\xfc\x1c\n" + "\x12RoomCommandService\x12Q\n" + "\n" + "CreateRoom\x12 .hyapp.room.v1.CreateRoomRequest\x1a!.hyapp.room.v1.CreateRoomResponse\x12f\n" + "\x11UpdateRoomProfile\x12'.hyapp.room.v1.UpdateRoomProfileRequest\x1a(.hyapp.room.v1.UpdateRoomProfileResponse\x12i\n" + "\x12SaveRoomBackground\x12(.hyapp.room.v1.SaveRoomBackgroundRequest\x1a).hyapp.room.v1.SaveRoomBackgroundResponse\x12f\n" + - "\x11SetRoomBackground\x12'.hyapp.room.v1.SetRoomBackgroundRequest\x1a(.hyapp.room.v1.SetRoomBackgroundResponse\x12K\n" + + "\x11SetRoomBackground\x12'.hyapp.room.v1.SetRoomBackgroundRequest\x1a(.hyapp.room.v1.SetRoomBackgroundResponse\x12l\n" + + "\x13ApplyRoomDecoration\x12).hyapp.room.v1.ApplyRoomDecorationRequest\x1a*.hyapp.room.v1.ApplyRoomDecorationResponse\x12Z\n" + + "\rSendRoomImage\x12#.hyapp.room.v1.SendRoomImageRequest\x1a$.hyapp.room.v1.SendRoomImageResponse\x12K\n" + "\bJoinRoom\x12\x1e.hyapp.room.v1.JoinRoomRequest\x1a\x1f.hyapp.room.v1.JoinRoomResponse\x12Z\n" + "\rRoomHeartbeat\x12#.hyapp.room.v1.RoomHeartbeatRequest\x1a$.hyapp.room.v1.RoomHeartbeatResponse\x12N\n" + "\tLeaveRoom\x12\x1f.hyapp.room.v1.LeaveRoomRequest\x1a .hyapp.room.v1.LeaveRoomResponse\x12N\n" + @@ -13756,11 +15132,13 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\bSendGift\x12\x1e.hyapp.room.v1.SendGiftRequest\x1a\x1f.hyapp.room.v1.SendGiftResponse\x12Q\n" + "\n" + "FollowRoom\x12 .hyapp.room.v1.FollowRoomRequest\x1a!.hyapp.room.v1.FollowRoomResponse\x12W\n" + - "\fUnfollowRoom\x12\".hyapp.room.v1.UnfollowRoomRequest\x1a#.hyapp.room.v1.UnfollowRoomResponse2\xd9\x02\n" + + "\fUnfollowRoom\x12\".hyapp.room.v1.UnfollowRoomRequest\x1a#.hyapp.room.v1.UnfollowRoomResponse2\xd0\x04\n" + "\x10RoomGuardService\x12o\n" + "\x14CheckSpeakPermission\x12*.hyapp.room.v1.CheckSpeakPermissionRequest\x1a+.hyapp.room.v1.CheckSpeakPermissionResponse\x12i\n" + "\x12VerifyRoomPresence\x12(.hyapp.room.v1.VerifyRoomPresenceRequest\x1a).hyapp.room.v1.VerifyRoomPresenceResponse\x12i\n" + - "\x12ResolveRoomAppCode\x12(.hyapp.room.v1.ResolveRoomAppCodeRequest\x1a).hyapp.room.v1.ResolveRoomAppCodeResponse2\xe4\x10\n" + + "\x12ResolveRoomAppCode\x12(.hyapp.room.v1.ResolveRoomAppCodeRequest\x1a).hyapp.room.v1.ResolveRoomAppCodeResponse\x12{\n" + + "\x18AuthorizeRoomMediaUpload\x12..hyapp.room.v1.AuthorizeRoomMediaUploadRequest\x1a/.hyapp.room.v1.AuthorizeRoomMediaUploadResponse\x12x\n" + + "\x17CompleteRoomMediaUpload\x12-.hyapp.room.v1.CompleteRoomMediaUploadRequest\x1a..hyapp.room.v1.CompleteRoomMediaUploadResponse2\xf4\x11\n" + "\x10RoomQueryService\x12]\n" + "\x0eAdminListRooms\x12$.hyapp.room.v1.AdminListRoomsRequest\x1a%.hyapp.room.v1.AdminListRoomsResponse\x12W\n" + "\fAdminGetRoom\x12\".hyapp.room.v1.AdminGetRoomRequest\x1a#.hyapp.room.v1.AdminGetRoomResponse\x12{\n" + @@ -13776,7 +15154,8 @@ const file_proto_room_v1_room_proto_rawDesc = "" + "\x17ListRoomGiftLeaderboard\x12-.hyapp.room.v1.ListRoomGiftLeaderboardRequest\x1a..hyapp.room.v1.ListRoomGiftLeaderboardResponse\x12{\n" + "\x18ListRoomContributionRank\x12..hyapp.room.v1.ListRoomContributionRankRequest\x1a/.hyapp.room.v1.ListRoomContributionRankResponse\x12N\n" + "\tGetMyRoom\x12\x1f.hyapp.room.v1.GetMyRoomRequest\x1a .hyapp.room.v1.GetMyRoomResponse\x12]\n" + - "\x0eGetCurrentRoom\x12$.hyapp.room.v1.GetCurrentRoomRequest\x1a%.hyapp.room.v1.GetCurrentRoomResponse\x12`\n" + + "\x0eGetCurrentRoom\x12$.hyapp.room.v1.GetCurrentRoomRequest\x1a%.hyapp.room.v1.GetCurrentRoomResponse\x12\x8d\x01\n" + + "\x1eBatchGetUserVoiceRoomPresences\x124.hyapp.room.v1.BatchGetUserVoiceRoomPresencesRequest\x1a5.hyapp.room.v1.BatchGetUserVoiceRoomPresencesResponse\x12`\n" + "\x0fGetRoomSnapshot\x12%.hyapp.room.v1.GetRoomSnapshotRequest\x1a&.hyapp.room.v1.GetRoomSnapshotResponse\x12l\n" + "\x13ListRoomBackgrounds\x12).hyapp.room.v1.ListRoomBackgroundsRequest\x1a*.hyapp.room.v1.ListRoomBackgroundsResponse\x12Z\n" + "\rGetRoomRocket\x12#.hyapp.room.v1.GetRoomRocketRequest\x1a$.hyapp.room.v1.GetRoomRocketResponse\x12l\n" + @@ -13795,7 +15174,7 @@ func file_proto_room_v1_room_proto_rawDescGZIP() []byte { return file_proto_room_v1_room_proto_rawDescData } -var file_proto_room_v1_room_proto_msgTypes = make([]protoimpl.MessageInfo, 159) +var file_proto_room_v1_room_proto_msgTypes = make([]protoimpl.MessageInfo, 173) var file_proto_room_v1_room_proto_goTypes = []any{ (*RequestMeta)(nil), // 0: hyapp.room.v1.RequestMeta (*CommandResult)(nil), // 1: hyapp.room.v1.CommandResult @@ -13852,113 +15231,127 @@ var file_proto_room_v1_room_proto_goTypes = []any{ (*AdminFilterAvailableRoomRobotsResponse)(nil), // 52: hyapp.room.v1.AdminFilterAvailableRoomRobotsResponse (*RoomBanState)(nil), // 53: hyapp.room.v1.RoomBanState (*RoomSnapshot)(nil), // 54: hyapp.room.v1.RoomSnapshot - (*RoomBackgroundImage)(nil), // 55: hyapp.room.v1.RoomBackgroundImage - (*SaveRoomBackgroundRequest)(nil), // 56: hyapp.room.v1.SaveRoomBackgroundRequest - (*SaveRoomBackgroundResponse)(nil), // 57: hyapp.room.v1.SaveRoomBackgroundResponse - (*ListRoomBackgroundsRequest)(nil), // 58: hyapp.room.v1.ListRoomBackgroundsRequest - (*ListRoomBackgroundsResponse)(nil), // 59: hyapp.room.v1.ListRoomBackgroundsResponse - (*SetRoomBackgroundRequest)(nil), // 60: hyapp.room.v1.SetRoomBackgroundRequest - (*SetRoomBackgroundResponse)(nil), // 61: hyapp.room.v1.SetRoomBackgroundResponse - (*CreateRoomRequest)(nil), // 62: hyapp.room.v1.CreateRoomRequest - (*CreateRoomResponse)(nil), // 63: hyapp.room.v1.CreateRoomResponse - (*UpdateRoomProfileRequest)(nil), // 64: hyapp.room.v1.UpdateRoomProfileRequest - (*UpdateRoomProfileResponse)(nil), // 65: hyapp.room.v1.UpdateRoomProfileResponse - (*RoomEntryVehicleSnapshot)(nil), // 66: hyapp.room.v1.RoomEntryVehicleSnapshot - (*RoomUserDisplayProfile)(nil), // 67: hyapp.room.v1.RoomUserDisplayProfile - (*JoinRoomRequest)(nil), // 68: hyapp.room.v1.JoinRoomRequest - (*RoomEffectiveVipSnapshot)(nil), // 69: hyapp.room.v1.RoomEffectiveVipSnapshot - (*JoinRoomResponse)(nil), // 70: hyapp.room.v1.JoinRoomResponse - (*RoomHeartbeatRequest)(nil), // 71: hyapp.room.v1.RoomHeartbeatRequest - (*RoomHeartbeatResponse)(nil), // 72: hyapp.room.v1.RoomHeartbeatResponse - (*LeaveRoomRequest)(nil), // 73: hyapp.room.v1.LeaveRoomRequest - (*LeaveRoomResponse)(nil), // 74: hyapp.room.v1.LeaveRoomResponse - (*CloseRoomRequest)(nil), // 75: hyapp.room.v1.CloseRoomRequest - (*CloseRoomResponse)(nil), // 76: hyapp.room.v1.CloseRoomResponse - (*AdminRoomListItem)(nil), // 77: hyapp.room.v1.AdminRoomListItem - (*AdminListRoomsRequest)(nil), // 78: hyapp.room.v1.AdminListRoomsRequest - (*AdminListRoomsResponse)(nil), // 79: hyapp.room.v1.AdminListRoomsResponse - (*AdminGetRoomRequest)(nil), // 80: hyapp.room.v1.AdminGetRoomRequest - (*AdminGetRoomResponse)(nil), // 81: hyapp.room.v1.AdminGetRoomResponse - (*AdminUpdateRoomRequest)(nil), // 82: hyapp.room.v1.AdminUpdateRoomRequest - (*AdminUpdateRoomResponse)(nil), // 83: hyapp.room.v1.AdminUpdateRoomResponse - (*AdminDeleteRoomRequest)(nil), // 84: hyapp.room.v1.AdminDeleteRoomRequest - (*AdminDeleteRoomResponse)(nil), // 85: hyapp.room.v1.AdminDeleteRoomResponse - (*MicUpRequest)(nil), // 86: hyapp.room.v1.MicUpRequest - (*MicUpResponse)(nil), // 87: hyapp.room.v1.MicUpResponse - (*MicDownRequest)(nil), // 88: hyapp.room.v1.MicDownRequest - (*MicDownResponse)(nil), // 89: hyapp.room.v1.MicDownResponse - (*ChangeMicSeatRequest)(nil), // 90: hyapp.room.v1.ChangeMicSeatRequest - (*ChangeMicSeatResponse)(nil), // 91: hyapp.room.v1.ChangeMicSeatResponse - (*ConfirmMicPublishingRequest)(nil), // 92: hyapp.room.v1.ConfirmMicPublishingRequest - (*ConfirmMicPublishingResponse)(nil), // 93: hyapp.room.v1.ConfirmMicPublishingResponse - (*MicHeartbeatRequest)(nil), // 94: hyapp.room.v1.MicHeartbeatRequest - (*MicHeartbeatResponse)(nil), // 95: hyapp.room.v1.MicHeartbeatResponse - (*SetMicMuteRequest)(nil), // 96: hyapp.room.v1.SetMicMuteRequest - (*SetMicMuteResponse)(nil), // 97: hyapp.room.v1.SetMicMuteResponse - (*ApplyRTCEventRequest)(nil), // 98: hyapp.room.v1.ApplyRTCEventRequest - (*ApplyRTCEventResponse)(nil), // 99: hyapp.room.v1.ApplyRTCEventResponse - (*SetMicSeatLockRequest)(nil), // 100: hyapp.room.v1.SetMicSeatLockRequest - (*SetMicSeatLockResponse)(nil), // 101: hyapp.room.v1.SetMicSeatLockResponse - (*SetChatEnabledRequest)(nil), // 102: hyapp.room.v1.SetChatEnabledRequest - (*SetChatEnabledResponse)(nil), // 103: hyapp.room.v1.SetChatEnabledResponse - (*SetRoomPasswordRequest)(nil), // 104: hyapp.room.v1.SetRoomPasswordRequest - (*SetRoomPasswordResponse)(nil), // 105: hyapp.room.v1.SetRoomPasswordResponse - (*SetRoomAdminRequest)(nil), // 106: hyapp.room.v1.SetRoomAdminRequest - (*SetRoomAdminResponse)(nil), // 107: hyapp.room.v1.SetRoomAdminResponse - (*MuteUserRequest)(nil), // 108: hyapp.room.v1.MuteUserRequest - (*MuteUserResponse)(nil), // 109: hyapp.room.v1.MuteUserResponse - (*KickUserRequest)(nil), // 110: hyapp.room.v1.KickUserRequest - (*KickUserResponse)(nil), // 111: hyapp.room.v1.KickUserResponse - (*UnbanUserRequest)(nil), // 112: hyapp.room.v1.UnbanUserRequest - (*UnbanUserResponse)(nil), // 113: hyapp.room.v1.UnbanUserResponse - (*SystemEvictUserRequest)(nil), // 114: hyapp.room.v1.SystemEvictUserRequest - (*SystemEvictUserResponse)(nil), // 115: hyapp.room.v1.SystemEvictUserResponse - (*SendGiftTargetHostScope)(nil), // 116: hyapp.room.v1.SendGiftTargetHostScope - (*SendGiftDisplayProfile)(nil), // 117: hyapp.room.v1.SendGiftDisplayProfile - (*SendGiftBatchTarget)(nil), // 118: hyapp.room.v1.SendGiftBatchTarget - (*SendGiftBatchDisplay)(nil), // 119: hyapp.room.v1.SendGiftBatchDisplay - (*SendGiftRequest)(nil), // 120: hyapp.room.v1.SendGiftRequest - (*SendGiftResponse)(nil), // 121: hyapp.room.v1.SendGiftResponse - (*CheckSpeakPermissionRequest)(nil), // 122: hyapp.room.v1.CheckSpeakPermissionRequest - (*CheckSpeakPermissionResponse)(nil), // 123: hyapp.room.v1.CheckSpeakPermissionResponse - (*VerifyRoomPresenceRequest)(nil), // 124: hyapp.room.v1.VerifyRoomPresenceRequest - (*VerifyRoomPresenceResponse)(nil), // 125: hyapp.room.v1.VerifyRoomPresenceResponse - (*ResolveRoomAppCodeRequest)(nil), // 126: hyapp.room.v1.ResolveRoomAppCodeRequest - (*ResolveRoomAppCodeResponse)(nil), // 127: hyapp.room.v1.ResolveRoomAppCodeResponse - (*ListRoomsRequest)(nil), // 128: hyapp.room.v1.ListRoomsRequest - (*ListRoomsByOwnersRequest)(nil), // 129: hyapp.room.v1.ListRoomsByOwnersRequest - (*ListRoomFeedsRequest)(nil), // 130: hyapp.room.v1.ListRoomFeedsRequest - (*ListRoomGiftLeaderboardRequest)(nil), // 131: hyapp.room.v1.ListRoomGiftLeaderboardRequest - (*RoomFeedRelatedUser)(nil), // 132: hyapp.room.v1.RoomFeedRelatedUser - (*RoomListItem)(nil), // 133: hyapp.room.v1.RoomListItem - (*ListRoomsResponse)(nil), // 134: hyapp.room.v1.ListRoomsResponse - (*RoomGiftLeaderboardItem)(nil), // 135: hyapp.room.v1.RoomGiftLeaderboardItem - (*ListRoomGiftLeaderboardResponse)(nil), // 136: hyapp.room.v1.ListRoomGiftLeaderboardResponse - (*ListRoomContributionRankRequest)(nil), // 137: hyapp.room.v1.ListRoomContributionRankRequest - (*ListRoomContributionRankResponse)(nil), // 138: hyapp.room.v1.ListRoomContributionRankResponse - (*GetMyRoomRequest)(nil), // 139: hyapp.room.v1.GetMyRoomRequest - (*GetMyRoomResponse)(nil), // 140: hyapp.room.v1.GetMyRoomResponse - (*GetCurrentRoomRequest)(nil), // 141: hyapp.room.v1.GetCurrentRoomRequest - (*GetCurrentRoomResponse)(nil), // 142: hyapp.room.v1.GetCurrentRoomResponse - (*GetRoomSnapshotRequest)(nil), // 143: hyapp.room.v1.GetRoomSnapshotRequest - (*GetRoomSnapshotResponse)(nil), // 144: hyapp.room.v1.GetRoomSnapshotResponse - (*GetRoomRocketRequest)(nil), // 145: hyapp.room.v1.GetRoomRocketRequest - (*GetRoomRocketResponse)(nil), // 146: hyapp.room.v1.GetRoomRocketResponse - (*ListRoomOnlineUsersRequest)(nil), // 147: hyapp.room.v1.ListRoomOnlineUsersRequest - (*ListRoomOnlineUsersResponse)(nil), // 148: hyapp.room.v1.ListRoomOnlineUsersResponse - (*RoomBannedUser)(nil), // 149: hyapp.room.v1.RoomBannedUser - (*ListRoomBannedUsersRequest)(nil), // 150: hyapp.room.v1.ListRoomBannedUsersRequest - (*ListRoomBannedUsersResponse)(nil), // 151: hyapp.room.v1.ListRoomBannedUsersResponse - (*FollowRoomRequest)(nil), // 152: hyapp.room.v1.FollowRoomRequest - (*FollowRoomResponse)(nil), // 153: hyapp.room.v1.FollowRoomResponse - (*UnfollowRoomRequest)(nil), // 154: hyapp.room.v1.UnfollowRoomRequest - (*UnfollowRoomResponse)(nil), // 155: hyapp.room.v1.UnfollowRoomResponse - (*SendGiftLuckyHit)(nil), // 156: hyapp.room.v1.SendGiftLuckyHit - (*SendGiftResultV2)(nil), // 157: hyapp.room.v1.SendGiftResultV2 - nil, // 158: hyapp.room.v1.RoomSnapshot.RoomExtEntry + (*RoomMedia)(nil), // 55: hyapp.room.v1.RoomMedia + (*RoomBackgroundImage)(nil), // 56: hyapp.room.v1.RoomBackgroundImage + (*SaveRoomBackgroundRequest)(nil), // 57: hyapp.room.v1.SaveRoomBackgroundRequest + (*SaveRoomBackgroundResponse)(nil), // 58: hyapp.room.v1.SaveRoomBackgroundResponse + (*ListRoomBackgroundsRequest)(nil), // 59: hyapp.room.v1.ListRoomBackgroundsRequest + (*ListRoomBackgroundsResponse)(nil), // 60: hyapp.room.v1.ListRoomBackgroundsResponse + (*SetRoomBackgroundRequest)(nil), // 61: hyapp.room.v1.SetRoomBackgroundRequest + (*SetRoomBackgroundResponse)(nil), // 62: hyapp.room.v1.SetRoomBackgroundResponse + (*RoomDecorationResource)(nil), // 63: hyapp.room.v1.RoomDecorationResource + (*ApplyRoomDecorationRequest)(nil), // 64: hyapp.room.v1.ApplyRoomDecorationRequest + (*ApplyRoomDecorationResponse)(nil), // 65: hyapp.room.v1.ApplyRoomDecorationResponse + (*AuthorizeRoomMediaUploadRequest)(nil), // 66: hyapp.room.v1.AuthorizeRoomMediaUploadRequest + (*AuthorizeRoomMediaUploadResponse)(nil), // 67: hyapp.room.v1.AuthorizeRoomMediaUploadResponse + (*CompleteRoomMediaUploadRequest)(nil), // 68: hyapp.room.v1.CompleteRoomMediaUploadRequest + (*CompleteRoomMediaUploadResponse)(nil), // 69: hyapp.room.v1.CompleteRoomMediaUploadResponse + (*SendRoomImageRequest)(nil), // 70: hyapp.room.v1.SendRoomImageRequest + (*SendRoomImageResponse)(nil), // 71: hyapp.room.v1.SendRoomImageResponse + (*CreateRoomRequest)(nil), // 72: hyapp.room.v1.CreateRoomRequest + (*CreateRoomResponse)(nil), // 73: hyapp.room.v1.CreateRoomResponse + (*UpdateRoomProfileRequest)(nil), // 74: hyapp.room.v1.UpdateRoomProfileRequest + (*UpdateRoomProfileResponse)(nil), // 75: hyapp.room.v1.UpdateRoomProfileResponse + (*RoomEntryVehicleSnapshot)(nil), // 76: hyapp.room.v1.RoomEntryVehicleSnapshot + (*RoomUserDisplayProfile)(nil), // 77: hyapp.room.v1.RoomUserDisplayProfile + (*JoinRoomRequest)(nil), // 78: hyapp.room.v1.JoinRoomRequest + (*RoomEffectiveVipSnapshot)(nil), // 79: hyapp.room.v1.RoomEffectiveVipSnapshot + (*JoinRoomResponse)(nil), // 80: hyapp.room.v1.JoinRoomResponse + (*RoomHeartbeatRequest)(nil), // 81: hyapp.room.v1.RoomHeartbeatRequest + (*RoomHeartbeatResponse)(nil), // 82: hyapp.room.v1.RoomHeartbeatResponse + (*LeaveRoomRequest)(nil), // 83: hyapp.room.v1.LeaveRoomRequest + (*LeaveRoomResponse)(nil), // 84: hyapp.room.v1.LeaveRoomResponse + (*CloseRoomRequest)(nil), // 85: hyapp.room.v1.CloseRoomRequest + (*CloseRoomResponse)(nil), // 86: hyapp.room.v1.CloseRoomResponse + (*AdminRoomListItem)(nil), // 87: hyapp.room.v1.AdminRoomListItem + (*AdminListRoomsRequest)(nil), // 88: hyapp.room.v1.AdminListRoomsRequest + (*AdminListRoomsResponse)(nil), // 89: hyapp.room.v1.AdminListRoomsResponse + (*AdminGetRoomRequest)(nil), // 90: hyapp.room.v1.AdminGetRoomRequest + (*AdminGetRoomResponse)(nil), // 91: hyapp.room.v1.AdminGetRoomResponse + (*AdminUpdateRoomRequest)(nil), // 92: hyapp.room.v1.AdminUpdateRoomRequest + (*AdminUpdateRoomResponse)(nil), // 93: hyapp.room.v1.AdminUpdateRoomResponse + (*AdminDeleteRoomRequest)(nil), // 94: hyapp.room.v1.AdminDeleteRoomRequest + (*AdminDeleteRoomResponse)(nil), // 95: hyapp.room.v1.AdminDeleteRoomResponse + (*MicUpRequest)(nil), // 96: hyapp.room.v1.MicUpRequest + (*MicUpResponse)(nil), // 97: hyapp.room.v1.MicUpResponse + (*MicDownRequest)(nil), // 98: hyapp.room.v1.MicDownRequest + (*MicDownResponse)(nil), // 99: hyapp.room.v1.MicDownResponse + (*ChangeMicSeatRequest)(nil), // 100: hyapp.room.v1.ChangeMicSeatRequest + (*ChangeMicSeatResponse)(nil), // 101: hyapp.room.v1.ChangeMicSeatResponse + (*ConfirmMicPublishingRequest)(nil), // 102: hyapp.room.v1.ConfirmMicPublishingRequest + (*ConfirmMicPublishingResponse)(nil), // 103: hyapp.room.v1.ConfirmMicPublishingResponse + (*MicHeartbeatRequest)(nil), // 104: hyapp.room.v1.MicHeartbeatRequest + (*MicHeartbeatResponse)(nil), // 105: hyapp.room.v1.MicHeartbeatResponse + (*SetMicMuteRequest)(nil), // 106: hyapp.room.v1.SetMicMuteRequest + (*SetMicMuteResponse)(nil), // 107: hyapp.room.v1.SetMicMuteResponse + (*ApplyRTCEventRequest)(nil), // 108: hyapp.room.v1.ApplyRTCEventRequest + (*ApplyRTCEventResponse)(nil), // 109: hyapp.room.v1.ApplyRTCEventResponse + (*SetMicSeatLockRequest)(nil), // 110: hyapp.room.v1.SetMicSeatLockRequest + (*SetMicSeatLockResponse)(nil), // 111: hyapp.room.v1.SetMicSeatLockResponse + (*SetChatEnabledRequest)(nil), // 112: hyapp.room.v1.SetChatEnabledRequest + (*SetChatEnabledResponse)(nil), // 113: hyapp.room.v1.SetChatEnabledResponse + (*SetRoomPasswordRequest)(nil), // 114: hyapp.room.v1.SetRoomPasswordRequest + (*SetRoomPasswordResponse)(nil), // 115: hyapp.room.v1.SetRoomPasswordResponse + (*SetRoomAdminRequest)(nil), // 116: hyapp.room.v1.SetRoomAdminRequest + (*SetRoomAdminResponse)(nil), // 117: hyapp.room.v1.SetRoomAdminResponse + (*MuteUserRequest)(nil), // 118: hyapp.room.v1.MuteUserRequest + (*MuteUserResponse)(nil), // 119: hyapp.room.v1.MuteUserResponse + (*KickUserRequest)(nil), // 120: hyapp.room.v1.KickUserRequest + (*KickUserResponse)(nil), // 121: hyapp.room.v1.KickUserResponse + (*UnbanUserRequest)(nil), // 122: hyapp.room.v1.UnbanUserRequest + (*UnbanUserResponse)(nil), // 123: hyapp.room.v1.UnbanUserResponse + (*SystemEvictUserRequest)(nil), // 124: hyapp.room.v1.SystemEvictUserRequest + (*SystemEvictUserResponse)(nil), // 125: hyapp.room.v1.SystemEvictUserResponse + (*SendGiftTargetHostScope)(nil), // 126: hyapp.room.v1.SendGiftTargetHostScope + (*SendGiftDisplayProfile)(nil), // 127: hyapp.room.v1.SendGiftDisplayProfile + (*SendGiftBatchTarget)(nil), // 128: hyapp.room.v1.SendGiftBatchTarget + (*SendGiftBatchDisplay)(nil), // 129: hyapp.room.v1.SendGiftBatchDisplay + (*SendGiftRequest)(nil), // 130: hyapp.room.v1.SendGiftRequest + (*SendGiftResponse)(nil), // 131: hyapp.room.v1.SendGiftResponse + (*CheckSpeakPermissionRequest)(nil), // 132: hyapp.room.v1.CheckSpeakPermissionRequest + (*CheckSpeakPermissionResponse)(nil), // 133: hyapp.room.v1.CheckSpeakPermissionResponse + (*VerifyRoomPresenceRequest)(nil), // 134: hyapp.room.v1.VerifyRoomPresenceRequest + (*VerifyRoomPresenceResponse)(nil), // 135: hyapp.room.v1.VerifyRoomPresenceResponse + (*ResolveRoomAppCodeRequest)(nil), // 136: hyapp.room.v1.ResolveRoomAppCodeRequest + (*ResolveRoomAppCodeResponse)(nil), // 137: hyapp.room.v1.ResolveRoomAppCodeResponse + (*ListRoomsRequest)(nil), // 138: hyapp.room.v1.ListRoomsRequest + (*ListRoomsByOwnersRequest)(nil), // 139: hyapp.room.v1.ListRoomsByOwnersRequest + (*ListRoomFeedsRequest)(nil), // 140: hyapp.room.v1.ListRoomFeedsRequest + (*ListRoomGiftLeaderboardRequest)(nil), // 141: hyapp.room.v1.ListRoomGiftLeaderboardRequest + (*RoomFeedRelatedUser)(nil), // 142: hyapp.room.v1.RoomFeedRelatedUser + (*RoomListItem)(nil), // 143: hyapp.room.v1.RoomListItem + (*ListRoomsResponse)(nil), // 144: hyapp.room.v1.ListRoomsResponse + (*RoomGiftLeaderboardItem)(nil), // 145: hyapp.room.v1.RoomGiftLeaderboardItem + (*ListRoomGiftLeaderboardResponse)(nil), // 146: hyapp.room.v1.ListRoomGiftLeaderboardResponse + (*ListRoomContributionRankRequest)(nil), // 147: hyapp.room.v1.ListRoomContributionRankRequest + (*ListRoomContributionRankResponse)(nil), // 148: hyapp.room.v1.ListRoomContributionRankResponse + (*GetMyRoomRequest)(nil), // 149: hyapp.room.v1.GetMyRoomRequest + (*GetMyRoomResponse)(nil), // 150: hyapp.room.v1.GetMyRoomResponse + (*GetCurrentRoomRequest)(nil), // 151: hyapp.room.v1.GetCurrentRoomRequest + (*GetCurrentRoomResponse)(nil), // 152: hyapp.room.v1.GetCurrentRoomResponse + (*BatchGetUserVoiceRoomPresencesRequest)(nil), // 153: hyapp.room.v1.BatchGetUserVoiceRoomPresencesRequest + (*UserVoiceRoomPresenceRoom)(nil), // 154: hyapp.room.v1.UserVoiceRoomPresenceRoom + (*UserVoiceRoomPresence)(nil), // 155: hyapp.room.v1.UserVoiceRoomPresence + (*BatchGetUserVoiceRoomPresencesResponse)(nil), // 156: hyapp.room.v1.BatchGetUserVoiceRoomPresencesResponse + (*GetRoomSnapshotRequest)(nil), // 157: hyapp.room.v1.GetRoomSnapshotRequest + (*GetRoomSnapshotResponse)(nil), // 158: hyapp.room.v1.GetRoomSnapshotResponse + (*GetRoomRocketRequest)(nil), // 159: hyapp.room.v1.GetRoomRocketRequest + (*GetRoomRocketResponse)(nil), // 160: hyapp.room.v1.GetRoomRocketResponse + (*ListRoomOnlineUsersRequest)(nil), // 161: hyapp.room.v1.ListRoomOnlineUsersRequest + (*ListRoomOnlineUsersResponse)(nil), // 162: hyapp.room.v1.ListRoomOnlineUsersResponse + (*RoomBannedUser)(nil), // 163: hyapp.room.v1.RoomBannedUser + (*ListRoomBannedUsersRequest)(nil), // 164: hyapp.room.v1.ListRoomBannedUsersRequest + (*ListRoomBannedUsersResponse)(nil), // 165: hyapp.room.v1.ListRoomBannedUsersResponse + (*FollowRoomRequest)(nil), // 166: hyapp.room.v1.FollowRoomRequest + (*FollowRoomResponse)(nil), // 167: hyapp.room.v1.FollowRoomResponse + (*UnfollowRoomRequest)(nil), // 168: hyapp.room.v1.UnfollowRoomRequest + (*UnfollowRoomResponse)(nil), // 169: hyapp.room.v1.UnfollowRoomResponse + (*SendGiftLuckyHit)(nil), // 170: hyapp.room.v1.SendGiftLuckyHit + (*SendGiftResultV2)(nil), // 171: hyapp.room.v1.SendGiftResultV2 + nil, // 172: hyapp.room.v1.RoomSnapshot.RoomExtEntry } var file_proto_room_v1_room_proto_depIdxs = []int32{ - 156, // 0: hyapp.room.v1.LuckyGiftDrawResult.hits:type_name -> hyapp.room.v1.SendGiftLuckyHit + 170, // 0: hyapp.room.v1.LuckyGiftDrawResult.hits:type_name -> hyapp.room.v1.SendGiftLuckyHit 8, // 1: hyapp.room.v1.RoomRocketLevel.in_room_rewards:type_name -> hyapp.room.v1.RoomRocketRewardItem 8, // 2: hyapp.room.v1.RoomRocketLevel.top1_rewards:type_name -> hyapp.room.v1.RoomRocketRewardItem 8, // 3: hyapp.room.v1.RoomRocketLevel.igniter_rewards:type_name -> hyapp.room.v1.RoomRocketRewardItem @@ -14005,266 +15398,300 @@ var file_proto_room_v1_room_proto_depIdxs = []int32{ 4, // 44: hyapp.room.v1.RoomSnapshot.mic_seats:type_name -> hyapp.room.v1.SeatState 2, // 45: hyapp.room.v1.RoomSnapshot.online_users:type_name -> hyapp.room.v1.RoomUser 5, // 46: hyapp.room.v1.RoomSnapshot.gift_rank:type_name -> hyapp.room.v1.RankItem - 158, // 47: hyapp.room.v1.RoomSnapshot.room_ext:type_name -> hyapp.room.v1.RoomSnapshot.RoomExtEntry + 172, // 47: hyapp.room.v1.RoomSnapshot.room_ext:type_name -> hyapp.room.v1.RoomSnapshot.RoomExtEntry 13, // 48: hyapp.room.v1.RoomSnapshot.rocket:type_name -> hyapp.room.v1.RoomRocketState 53, // 49: hyapp.room.v1.RoomSnapshot.ban_states:type_name -> hyapp.room.v1.RoomBanState - 0, // 50: hyapp.room.v1.SaveRoomBackgroundRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 55, // 51: hyapp.room.v1.SaveRoomBackgroundResponse.background:type_name -> hyapp.room.v1.RoomBackgroundImage - 0, // 52: hyapp.room.v1.ListRoomBackgroundsRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 55, // 53: hyapp.room.v1.ListRoomBackgroundsResponse.backgrounds:type_name -> hyapp.room.v1.RoomBackgroundImage - 0, // 54: hyapp.room.v1.SetRoomBackgroundRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 55: hyapp.room.v1.SetRoomBackgroundResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 56: hyapp.room.v1.SetRoomBackgroundResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 55, // 57: hyapp.room.v1.SetRoomBackgroundResponse.background:type_name -> hyapp.room.v1.RoomBackgroundImage - 0, // 58: hyapp.room.v1.CreateRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 59: hyapp.room.v1.CreateRoomResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 60: hyapp.room.v1.CreateRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 61: hyapp.room.v1.UpdateRoomProfileRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 62: hyapp.room.v1.UpdateRoomProfileResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 63: hyapp.room.v1.UpdateRoomProfileResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 64: hyapp.room.v1.JoinRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 66, // 65: hyapp.room.v1.JoinRoomRequest.entry_vehicle:type_name -> hyapp.room.v1.RoomEntryVehicleSnapshot - 67, // 66: hyapp.room.v1.JoinRoomRequest.actor_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile - 1, // 67: hyapp.room.v1.JoinRoomResponse.result:type_name -> hyapp.room.v1.CommandResult - 2, // 68: hyapp.room.v1.JoinRoomResponse.user:type_name -> hyapp.room.v1.RoomUser - 54, // 69: hyapp.room.v1.JoinRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 69, // 70: hyapp.room.v1.JoinRoomResponse.effective_vip:type_name -> hyapp.room.v1.RoomEffectiveVipSnapshot - 0, // 71: hyapp.room.v1.RoomHeartbeatRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 72: hyapp.room.v1.RoomHeartbeatResponse.result:type_name -> hyapp.room.v1.CommandResult - 2, // 73: hyapp.room.v1.RoomHeartbeatResponse.user:type_name -> hyapp.room.v1.RoomUser - 54, // 74: hyapp.room.v1.RoomHeartbeatResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 75: hyapp.room.v1.LeaveRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 76: hyapp.room.v1.LeaveRoomResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 77: hyapp.room.v1.LeaveRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 78: hyapp.room.v1.CloseRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 79: hyapp.room.v1.CloseRoomResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 80: hyapp.room.v1.CloseRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 81: hyapp.room.v1.AdminListRoomsRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 77, // 82: hyapp.room.v1.AdminListRoomsResponse.rooms:type_name -> hyapp.room.v1.AdminRoomListItem - 0, // 83: hyapp.room.v1.AdminGetRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 77, // 84: hyapp.room.v1.AdminGetRoomResponse.room:type_name -> hyapp.room.v1.AdminRoomListItem - 0, // 85: hyapp.room.v1.AdminUpdateRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 86: hyapp.room.v1.AdminUpdateRoomResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 87: hyapp.room.v1.AdminUpdateRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 88: hyapp.room.v1.AdminDeleteRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 89: hyapp.room.v1.AdminDeleteRoomResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 90: hyapp.room.v1.AdminDeleteRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 91: hyapp.room.v1.MicUpRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 67, // 92: hyapp.room.v1.MicUpRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile - 1, // 93: hyapp.room.v1.MicUpResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 94: hyapp.room.v1.MicUpResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 95: hyapp.room.v1.MicDownRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 67, // 96: hyapp.room.v1.MicDownRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile - 1, // 97: hyapp.room.v1.MicDownResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 98: hyapp.room.v1.MicDownResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 99: hyapp.room.v1.ChangeMicSeatRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 67, // 100: hyapp.room.v1.ChangeMicSeatRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile - 1, // 101: hyapp.room.v1.ChangeMicSeatResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 102: hyapp.room.v1.ChangeMicSeatResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 103: hyapp.room.v1.ConfirmMicPublishingRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 67, // 104: hyapp.room.v1.ConfirmMicPublishingRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile - 1, // 105: hyapp.room.v1.ConfirmMicPublishingResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 106: hyapp.room.v1.ConfirmMicPublishingResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 107: hyapp.room.v1.MicHeartbeatRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 108: hyapp.room.v1.MicHeartbeatResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 109: hyapp.room.v1.MicHeartbeatResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 110: hyapp.room.v1.SetMicMuteRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 67, // 111: hyapp.room.v1.SetMicMuteRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile - 1, // 112: hyapp.room.v1.SetMicMuteResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 113: hyapp.room.v1.SetMicMuteResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 114: hyapp.room.v1.ApplyRTCEventRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 115: hyapp.room.v1.ApplyRTCEventResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 116: hyapp.room.v1.ApplyRTCEventResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 117: hyapp.room.v1.SetMicSeatLockRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 118: hyapp.room.v1.SetMicSeatLockResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 119: hyapp.room.v1.SetMicSeatLockResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 120: hyapp.room.v1.SetChatEnabledRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 121: hyapp.room.v1.SetChatEnabledResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 122: hyapp.room.v1.SetChatEnabledResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 123: hyapp.room.v1.SetRoomPasswordRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 124: hyapp.room.v1.SetRoomPasswordResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 125: hyapp.room.v1.SetRoomPasswordResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 126: hyapp.room.v1.SetRoomAdminRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 127: hyapp.room.v1.SetRoomAdminResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 128: hyapp.room.v1.SetRoomAdminResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 129: hyapp.room.v1.MuteUserRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 130: hyapp.room.v1.MuteUserResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 131: hyapp.room.v1.MuteUserResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 132: hyapp.room.v1.KickUserRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 133: hyapp.room.v1.KickUserResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 134: hyapp.room.v1.KickUserResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 135: hyapp.room.v1.UnbanUserRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 136: hyapp.room.v1.UnbanUserResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 137: hyapp.room.v1.UnbanUserResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 138: hyapp.room.v1.SystemEvictUserRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 1, // 139: hyapp.room.v1.SystemEvictUserResponse.result:type_name -> hyapp.room.v1.CommandResult - 54, // 140: hyapp.room.v1.SystemEvictUserResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 7, // 141: hyapp.room.v1.SendGiftBatchTarget.lucky_gift:type_name -> hyapp.room.v1.LuckyGiftDrawResult - 118, // 142: hyapp.room.v1.SendGiftBatchDisplay.targets:type_name -> hyapp.room.v1.SendGiftBatchTarget - 0, // 143: hyapp.room.v1.SendGiftRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 116, // 144: hyapp.room.v1.SendGiftRequest.target_host_scopes:type_name -> hyapp.room.v1.SendGiftTargetHostScope - 117, // 145: hyapp.room.v1.SendGiftRequest.sender_display_profile:type_name -> hyapp.room.v1.SendGiftDisplayProfile - 117, // 146: hyapp.room.v1.SendGiftRequest.target_display_profiles:type_name -> hyapp.room.v1.SendGiftDisplayProfile - 1, // 147: hyapp.room.v1.SendGiftResponse.result:type_name -> hyapp.room.v1.CommandResult - 5, // 148: hyapp.room.v1.SendGiftResponse.gift_rank:type_name -> hyapp.room.v1.RankItem - 54, // 149: hyapp.room.v1.SendGiftResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 13, // 150: hyapp.room.v1.SendGiftResponse.rocket:type_name -> hyapp.room.v1.RoomRocketState - 7, // 151: hyapp.room.v1.SendGiftResponse.lucky_gift:type_name -> hyapp.room.v1.LuckyGiftDrawResult - 7, // 152: hyapp.room.v1.SendGiftResponse.lucky_gifts:type_name -> hyapp.room.v1.LuckyGiftDrawResult - 119, // 153: hyapp.room.v1.SendGiftResponse.batch_display:type_name -> hyapp.room.v1.SendGiftBatchDisplay - 157, // 154: hyapp.room.v1.SendGiftResponse.v2:type_name -> hyapp.room.v1.SendGiftResultV2 - 0, // 155: hyapp.room.v1.ListRoomsRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 0, // 156: hyapp.room.v1.ListRoomsByOwnersRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 0, // 157: hyapp.room.v1.ListRoomFeedsRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 132, // 158: hyapp.room.v1.ListRoomFeedsRequest.related_users:type_name -> hyapp.room.v1.RoomFeedRelatedUser - 0, // 159: hyapp.room.v1.ListRoomGiftLeaderboardRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 133, // 160: hyapp.room.v1.ListRoomsResponse.rooms:type_name -> hyapp.room.v1.RoomListItem - 133, // 161: hyapp.room.v1.RoomGiftLeaderboardItem.room:type_name -> hyapp.room.v1.RoomListItem - 135, // 162: hyapp.room.v1.ListRoomGiftLeaderboardResponse.items:type_name -> hyapp.room.v1.RoomGiftLeaderboardItem - 0, // 163: hyapp.room.v1.ListRoomContributionRankRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 6, // 164: hyapp.room.v1.ListRoomContributionRankResponse.items:type_name -> hyapp.room.v1.RoomContributionRankItem - 0, // 165: hyapp.room.v1.GetMyRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 133, // 166: hyapp.room.v1.GetMyRoomResponse.room:type_name -> hyapp.room.v1.RoomListItem - 0, // 167: hyapp.room.v1.GetCurrentRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 0, // 168: hyapp.room.v1.GetRoomSnapshotRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 54, // 169: hyapp.room.v1.GetRoomSnapshotResponse.room:type_name -> hyapp.room.v1.RoomSnapshot - 0, // 170: hyapp.room.v1.GetRoomRocketRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 14, // 171: hyapp.room.v1.GetRoomRocketResponse.rocket:type_name -> hyapp.room.v1.RoomRocketInfo - 0, // 172: hyapp.room.v1.ListRoomOnlineUsersRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 2, // 173: hyapp.room.v1.ListRoomOnlineUsersResponse.users:type_name -> hyapp.room.v1.RoomUser - 3, // 174: hyapp.room.v1.ListRoomOnlineUsersResponse.items:type_name -> hyapp.room.v1.RoomOnlineUser - 0, // 175: hyapp.room.v1.ListRoomBannedUsersRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 149, // 176: hyapp.room.v1.ListRoomBannedUsersResponse.items:type_name -> hyapp.room.v1.RoomBannedUser - 0, // 177: hyapp.room.v1.FollowRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 0, // 178: hyapp.room.v1.UnfollowRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta - 7, // 179: hyapp.room.v1.SendGiftResultV2.lucky_gift:type_name -> hyapp.room.v1.LuckyGiftDrawResult - 7, // 180: hyapp.room.v1.SendGiftResultV2.lucky_gifts:type_name -> hyapp.room.v1.LuckyGiftDrawResult - 156, // 181: hyapp.room.v1.SendGiftResultV2.lucky_hits:type_name -> hyapp.room.v1.SendGiftLuckyHit - 119, // 182: hyapp.room.v1.SendGiftResultV2.batch_display:type_name -> hyapp.room.v1.SendGiftBatchDisplay - 5, // 183: hyapp.room.v1.SendGiftResultV2.gift_rank:type_name -> hyapp.room.v1.RankItem - 62, // 184: hyapp.room.v1.RoomCommandService.CreateRoom:input_type -> hyapp.room.v1.CreateRoomRequest - 64, // 185: hyapp.room.v1.RoomCommandService.UpdateRoomProfile:input_type -> hyapp.room.v1.UpdateRoomProfileRequest - 56, // 186: hyapp.room.v1.RoomCommandService.SaveRoomBackground:input_type -> hyapp.room.v1.SaveRoomBackgroundRequest - 60, // 187: hyapp.room.v1.RoomCommandService.SetRoomBackground:input_type -> hyapp.room.v1.SetRoomBackgroundRequest - 68, // 188: hyapp.room.v1.RoomCommandService.JoinRoom:input_type -> hyapp.room.v1.JoinRoomRequest - 71, // 189: hyapp.room.v1.RoomCommandService.RoomHeartbeat:input_type -> hyapp.room.v1.RoomHeartbeatRequest - 73, // 190: hyapp.room.v1.RoomCommandService.LeaveRoom:input_type -> hyapp.room.v1.LeaveRoomRequest - 75, // 191: hyapp.room.v1.RoomCommandService.CloseRoom:input_type -> hyapp.room.v1.CloseRoomRequest - 82, // 192: hyapp.room.v1.RoomCommandService.AdminUpdateRoom:input_type -> hyapp.room.v1.AdminUpdateRoomRequest - 84, // 193: hyapp.room.v1.RoomCommandService.AdminDeleteRoom:input_type -> hyapp.room.v1.AdminDeleteRoomRequest - 19, // 194: hyapp.room.v1.RoomCommandService.AdminUpdateRoomRocketConfig:input_type -> hyapp.room.v1.AdminUpdateRoomRocketConfigRequest - 24, // 195: hyapp.room.v1.RoomCommandService.AdminUpdateRoomSeatConfig:input_type -> hyapp.room.v1.AdminUpdateRoomSeatConfigRequest - 31, // 196: hyapp.room.v1.RoomCommandService.AdminUpdateHumanRoomRobotConfig:input_type -> hyapp.room.v1.AdminUpdateHumanRoomRobotConfigRequest - 37, // 197: hyapp.room.v1.RoomCommandService.AdminCreateRoomPin:input_type -> hyapp.room.v1.AdminCreateRoomPinRequest - 39, // 198: hyapp.room.v1.RoomCommandService.AdminCancelRoomPin:input_type -> hyapp.room.v1.AdminCancelRoomPinRequest - 45, // 199: hyapp.room.v1.RoomCommandService.AdminCreateRobotRoom:input_type -> hyapp.room.v1.AdminCreateRobotRoomRequest - 47, // 200: hyapp.room.v1.RoomCommandService.AdminSetRobotRoomStatus:input_type -> hyapp.room.v1.AdminSetRobotRoomStatusRequest - 49, // 201: hyapp.room.v1.RoomCommandService.AdminRenameOwnerCountryCode:input_type -> hyapp.room.v1.AdminRenameOwnerCountryCodeRequest - 86, // 202: hyapp.room.v1.RoomCommandService.MicUp:input_type -> hyapp.room.v1.MicUpRequest - 88, // 203: hyapp.room.v1.RoomCommandService.MicDown:input_type -> hyapp.room.v1.MicDownRequest - 90, // 204: hyapp.room.v1.RoomCommandService.ChangeMicSeat:input_type -> hyapp.room.v1.ChangeMicSeatRequest - 92, // 205: hyapp.room.v1.RoomCommandService.ConfirmMicPublishing:input_type -> hyapp.room.v1.ConfirmMicPublishingRequest - 94, // 206: hyapp.room.v1.RoomCommandService.MicHeartbeat:input_type -> hyapp.room.v1.MicHeartbeatRequest - 96, // 207: hyapp.room.v1.RoomCommandService.SetMicMute:input_type -> hyapp.room.v1.SetMicMuteRequest - 98, // 208: hyapp.room.v1.RoomCommandService.ApplyRTCEvent:input_type -> hyapp.room.v1.ApplyRTCEventRequest - 100, // 209: hyapp.room.v1.RoomCommandService.SetMicSeatLock:input_type -> hyapp.room.v1.SetMicSeatLockRequest - 102, // 210: hyapp.room.v1.RoomCommandService.SetChatEnabled:input_type -> hyapp.room.v1.SetChatEnabledRequest - 104, // 211: hyapp.room.v1.RoomCommandService.SetRoomPassword:input_type -> hyapp.room.v1.SetRoomPasswordRequest - 106, // 212: hyapp.room.v1.RoomCommandService.SetRoomAdmin:input_type -> hyapp.room.v1.SetRoomAdminRequest - 108, // 213: hyapp.room.v1.RoomCommandService.MuteUser:input_type -> hyapp.room.v1.MuteUserRequest - 110, // 214: hyapp.room.v1.RoomCommandService.KickUser:input_type -> hyapp.room.v1.KickUserRequest - 112, // 215: hyapp.room.v1.RoomCommandService.UnbanUser:input_type -> hyapp.room.v1.UnbanUserRequest - 114, // 216: hyapp.room.v1.RoomCommandService.SystemEvictUser:input_type -> hyapp.room.v1.SystemEvictUserRequest - 120, // 217: hyapp.room.v1.RoomCommandService.SendGift:input_type -> hyapp.room.v1.SendGiftRequest - 152, // 218: hyapp.room.v1.RoomCommandService.FollowRoom:input_type -> hyapp.room.v1.FollowRoomRequest - 154, // 219: hyapp.room.v1.RoomCommandService.UnfollowRoom:input_type -> hyapp.room.v1.UnfollowRoomRequest - 122, // 220: hyapp.room.v1.RoomGuardService.CheckSpeakPermission:input_type -> hyapp.room.v1.CheckSpeakPermissionRequest - 124, // 221: hyapp.room.v1.RoomGuardService.VerifyRoomPresence:input_type -> hyapp.room.v1.VerifyRoomPresenceRequest - 126, // 222: hyapp.room.v1.RoomGuardService.ResolveRoomAppCode:input_type -> hyapp.room.v1.ResolveRoomAppCodeRequest - 78, // 223: hyapp.room.v1.RoomQueryService.AdminListRooms:input_type -> hyapp.room.v1.AdminListRoomsRequest - 80, // 224: hyapp.room.v1.RoomQueryService.AdminGetRoom:input_type -> hyapp.room.v1.AdminGetRoomRequest - 17, // 225: hyapp.room.v1.RoomQueryService.AdminGetRoomRocketConfig:input_type -> hyapp.room.v1.AdminGetRoomRocketConfigRequest - 22, // 226: hyapp.room.v1.RoomQueryService.AdminGetRoomSeatConfig:input_type -> hyapp.room.v1.AdminGetRoomSeatConfigRequest - 29, // 227: hyapp.room.v1.RoomQueryService.AdminGetHumanRoomRobotConfig:input_type -> hyapp.room.v1.AdminGetHumanRoomRobotConfigRequest - 35, // 228: hyapp.room.v1.RoomQueryService.AdminListRoomPins:input_type -> hyapp.room.v1.AdminListRoomPinsRequest - 43, // 229: hyapp.room.v1.RoomQueryService.AdminListRobotRooms:input_type -> hyapp.room.v1.AdminListRobotRoomsRequest - 51, // 230: hyapp.room.v1.RoomQueryService.AdminFilterAvailableRoomRobots:input_type -> hyapp.room.v1.AdminFilterAvailableRoomRobotsRequest - 128, // 231: hyapp.room.v1.RoomQueryService.ListRooms:input_type -> hyapp.room.v1.ListRoomsRequest - 129, // 232: hyapp.room.v1.RoomQueryService.ListRoomsByOwners:input_type -> hyapp.room.v1.ListRoomsByOwnersRequest - 130, // 233: hyapp.room.v1.RoomQueryService.ListRoomFeeds:input_type -> hyapp.room.v1.ListRoomFeedsRequest - 131, // 234: hyapp.room.v1.RoomQueryService.ListRoomGiftLeaderboard:input_type -> hyapp.room.v1.ListRoomGiftLeaderboardRequest - 137, // 235: hyapp.room.v1.RoomQueryService.ListRoomContributionRank:input_type -> hyapp.room.v1.ListRoomContributionRankRequest - 139, // 236: hyapp.room.v1.RoomQueryService.GetMyRoom:input_type -> hyapp.room.v1.GetMyRoomRequest - 141, // 237: hyapp.room.v1.RoomQueryService.GetCurrentRoom:input_type -> hyapp.room.v1.GetCurrentRoomRequest - 143, // 238: hyapp.room.v1.RoomQueryService.GetRoomSnapshot:input_type -> hyapp.room.v1.GetRoomSnapshotRequest - 58, // 239: hyapp.room.v1.RoomQueryService.ListRoomBackgrounds:input_type -> hyapp.room.v1.ListRoomBackgroundsRequest - 145, // 240: hyapp.room.v1.RoomQueryService.GetRoomRocket:input_type -> hyapp.room.v1.GetRoomRocketRequest - 147, // 241: hyapp.room.v1.RoomQueryService.ListRoomOnlineUsers:input_type -> hyapp.room.v1.ListRoomOnlineUsersRequest - 150, // 242: hyapp.room.v1.RoomQueryService.ListRoomBannedUsers:input_type -> hyapp.room.v1.ListRoomBannedUsersRequest - 63, // 243: hyapp.room.v1.RoomCommandService.CreateRoom:output_type -> hyapp.room.v1.CreateRoomResponse - 65, // 244: hyapp.room.v1.RoomCommandService.UpdateRoomProfile:output_type -> hyapp.room.v1.UpdateRoomProfileResponse - 57, // 245: hyapp.room.v1.RoomCommandService.SaveRoomBackground:output_type -> hyapp.room.v1.SaveRoomBackgroundResponse - 61, // 246: hyapp.room.v1.RoomCommandService.SetRoomBackground:output_type -> hyapp.room.v1.SetRoomBackgroundResponse - 70, // 247: hyapp.room.v1.RoomCommandService.JoinRoom:output_type -> hyapp.room.v1.JoinRoomResponse - 72, // 248: hyapp.room.v1.RoomCommandService.RoomHeartbeat:output_type -> hyapp.room.v1.RoomHeartbeatResponse - 74, // 249: hyapp.room.v1.RoomCommandService.LeaveRoom:output_type -> hyapp.room.v1.LeaveRoomResponse - 76, // 250: hyapp.room.v1.RoomCommandService.CloseRoom:output_type -> hyapp.room.v1.CloseRoomResponse - 83, // 251: hyapp.room.v1.RoomCommandService.AdminUpdateRoom:output_type -> hyapp.room.v1.AdminUpdateRoomResponse - 85, // 252: hyapp.room.v1.RoomCommandService.AdminDeleteRoom:output_type -> hyapp.room.v1.AdminDeleteRoomResponse - 20, // 253: hyapp.room.v1.RoomCommandService.AdminUpdateRoomRocketConfig:output_type -> hyapp.room.v1.AdminUpdateRoomRocketConfigResponse - 25, // 254: hyapp.room.v1.RoomCommandService.AdminUpdateRoomSeatConfig:output_type -> hyapp.room.v1.AdminUpdateRoomSeatConfigResponse - 32, // 255: hyapp.room.v1.RoomCommandService.AdminUpdateHumanRoomRobotConfig:output_type -> hyapp.room.v1.AdminUpdateHumanRoomRobotConfigResponse - 38, // 256: hyapp.room.v1.RoomCommandService.AdminCreateRoomPin:output_type -> hyapp.room.v1.AdminCreateRoomPinResponse - 40, // 257: hyapp.room.v1.RoomCommandService.AdminCancelRoomPin:output_type -> hyapp.room.v1.AdminCancelRoomPinResponse - 46, // 258: hyapp.room.v1.RoomCommandService.AdminCreateRobotRoom:output_type -> hyapp.room.v1.AdminCreateRobotRoomResponse - 48, // 259: hyapp.room.v1.RoomCommandService.AdminSetRobotRoomStatus:output_type -> hyapp.room.v1.AdminSetRobotRoomStatusResponse - 50, // 260: hyapp.room.v1.RoomCommandService.AdminRenameOwnerCountryCode:output_type -> hyapp.room.v1.AdminRenameOwnerCountryCodeResponse - 87, // 261: hyapp.room.v1.RoomCommandService.MicUp:output_type -> hyapp.room.v1.MicUpResponse - 89, // 262: hyapp.room.v1.RoomCommandService.MicDown:output_type -> hyapp.room.v1.MicDownResponse - 91, // 263: hyapp.room.v1.RoomCommandService.ChangeMicSeat:output_type -> hyapp.room.v1.ChangeMicSeatResponse - 93, // 264: hyapp.room.v1.RoomCommandService.ConfirmMicPublishing:output_type -> hyapp.room.v1.ConfirmMicPublishingResponse - 95, // 265: hyapp.room.v1.RoomCommandService.MicHeartbeat:output_type -> hyapp.room.v1.MicHeartbeatResponse - 97, // 266: hyapp.room.v1.RoomCommandService.SetMicMute:output_type -> hyapp.room.v1.SetMicMuteResponse - 99, // 267: hyapp.room.v1.RoomCommandService.ApplyRTCEvent:output_type -> hyapp.room.v1.ApplyRTCEventResponse - 101, // 268: hyapp.room.v1.RoomCommandService.SetMicSeatLock:output_type -> hyapp.room.v1.SetMicSeatLockResponse - 103, // 269: hyapp.room.v1.RoomCommandService.SetChatEnabled:output_type -> hyapp.room.v1.SetChatEnabledResponse - 105, // 270: hyapp.room.v1.RoomCommandService.SetRoomPassword:output_type -> hyapp.room.v1.SetRoomPasswordResponse - 107, // 271: hyapp.room.v1.RoomCommandService.SetRoomAdmin:output_type -> hyapp.room.v1.SetRoomAdminResponse - 109, // 272: hyapp.room.v1.RoomCommandService.MuteUser:output_type -> hyapp.room.v1.MuteUserResponse - 111, // 273: hyapp.room.v1.RoomCommandService.KickUser:output_type -> hyapp.room.v1.KickUserResponse - 113, // 274: hyapp.room.v1.RoomCommandService.UnbanUser:output_type -> hyapp.room.v1.UnbanUserResponse - 115, // 275: hyapp.room.v1.RoomCommandService.SystemEvictUser:output_type -> hyapp.room.v1.SystemEvictUserResponse - 121, // 276: hyapp.room.v1.RoomCommandService.SendGift:output_type -> hyapp.room.v1.SendGiftResponse - 153, // 277: hyapp.room.v1.RoomCommandService.FollowRoom:output_type -> hyapp.room.v1.FollowRoomResponse - 155, // 278: hyapp.room.v1.RoomCommandService.UnfollowRoom:output_type -> hyapp.room.v1.UnfollowRoomResponse - 123, // 279: hyapp.room.v1.RoomGuardService.CheckSpeakPermission:output_type -> hyapp.room.v1.CheckSpeakPermissionResponse - 125, // 280: hyapp.room.v1.RoomGuardService.VerifyRoomPresence:output_type -> hyapp.room.v1.VerifyRoomPresenceResponse - 127, // 281: hyapp.room.v1.RoomGuardService.ResolveRoomAppCode:output_type -> hyapp.room.v1.ResolveRoomAppCodeResponse - 79, // 282: hyapp.room.v1.RoomQueryService.AdminListRooms:output_type -> hyapp.room.v1.AdminListRoomsResponse - 81, // 283: hyapp.room.v1.RoomQueryService.AdminGetRoom:output_type -> hyapp.room.v1.AdminGetRoomResponse - 18, // 284: hyapp.room.v1.RoomQueryService.AdminGetRoomRocketConfig:output_type -> hyapp.room.v1.AdminGetRoomRocketConfigResponse - 23, // 285: hyapp.room.v1.RoomQueryService.AdminGetRoomSeatConfig:output_type -> hyapp.room.v1.AdminGetRoomSeatConfigResponse - 30, // 286: hyapp.room.v1.RoomQueryService.AdminGetHumanRoomRobotConfig:output_type -> hyapp.room.v1.AdminGetHumanRoomRobotConfigResponse - 36, // 287: hyapp.room.v1.RoomQueryService.AdminListRoomPins:output_type -> hyapp.room.v1.AdminListRoomPinsResponse - 44, // 288: hyapp.room.v1.RoomQueryService.AdminListRobotRooms:output_type -> hyapp.room.v1.AdminListRobotRoomsResponse - 52, // 289: hyapp.room.v1.RoomQueryService.AdminFilterAvailableRoomRobots:output_type -> hyapp.room.v1.AdminFilterAvailableRoomRobotsResponse - 134, // 290: hyapp.room.v1.RoomQueryService.ListRooms:output_type -> hyapp.room.v1.ListRoomsResponse - 134, // 291: hyapp.room.v1.RoomQueryService.ListRoomsByOwners:output_type -> hyapp.room.v1.ListRoomsResponse - 134, // 292: hyapp.room.v1.RoomQueryService.ListRoomFeeds:output_type -> hyapp.room.v1.ListRoomsResponse - 136, // 293: hyapp.room.v1.RoomQueryService.ListRoomGiftLeaderboard:output_type -> hyapp.room.v1.ListRoomGiftLeaderboardResponse - 138, // 294: hyapp.room.v1.RoomQueryService.ListRoomContributionRank:output_type -> hyapp.room.v1.ListRoomContributionRankResponse - 140, // 295: hyapp.room.v1.RoomQueryService.GetMyRoom:output_type -> hyapp.room.v1.GetMyRoomResponse - 142, // 296: hyapp.room.v1.RoomQueryService.GetCurrentRoom:output_type -> hyapp.room.v1.GetCurrentRoomResponse - 144, // 297: hyapp.room.v1.RoomQueryService.GetRoomSnapshot:output_type -> hyapp.room.v1.GetRoomSnapshotResponse - 59, // 298: hyapp.room.v1.RoomQueryService.ListRoomBackgrounds:output_type -> hyapp.room.v1.ListRoomBackgroundsResponse - 146, // 299: hyapp.room.v1.RoomQueryService.GetRoomRocket:output_type -> hyapp.room.v1.GetRoomRocketResponse - 148, // 300: hyapp.room.v1.RoomQueryService.ListRoomOnlineUsers:output_type -> hyapp.room.v1.ListRoomOnlineUsersResponse - 151, // 301: hyapp.room.v1.RoomQueryService.ListRoomBannedUsers:output_type -> hyapp.room.v1.ListRoomBannedUsersResponse - 243, // [243:302] is the sub-list for method output_type - 184, // [184:243] is the sub-list for method input_type - 184, // [184:184] is the sub-list for extension type_name - 184, // [184:184] is the sub-list for extension extendee - 0, // [0:184] is the sub-list for field type_name + 63, // 50: hyapp.room.v1.RoomSnapshot.room_border:type_name -> hyapp.room.v1.RoomDecorationResource + 63, // 51: hyapp.room.v1.RoomSnapshot.room_name_style:type_name -> hyapp.room.v1.RoomDecorationResource + 56, // 52: hyapp.room.v1.RoomSnapshot.active_background:type_name -> hyapp.room.v1.RoomBackgroundImage + 55, // 53: hyapp.room.v1.RoomBackgroundImage.media:type_name -> hyapp.room.v1.RoomMedia + 0, // 54: hyapp.room.v1.SaveRoomBackgroundRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 55, // 55: hyapp.room.v1.SaveRoomBackgroundRequest.media:type_name -> hyapp.room.v1.RoomMedia + 56, // 56: hyapp.room.v1.SaveRoomBackgroundResponse.background:type_name -> hyapp.room.v1.RoomBackgroundImage + 0, // 57: hyapp.room.v1.ListRoomBackgroundsRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 56, // 58: hyapp.room.v1.ListRoomBackgroundsResponse.backgrounds:type_name -> hyapp.room.v1.RoomBackgroundImage + 56, // 59: hyapp.room.v1.ListRoomBackgroundsResponse.selected_background:type_name -> hyapp.room.v1.RoomBackgroundImage + 0, // 60: hyapp.room.v1.SetRoomBackgroundRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 61: hyapp.room.v1.SetRoomBackgroundResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 62: hyapp.room.v1.SetRoomBackgroundResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 56, // 63: hyapp.room.v1.SetRoomBackgroundResponse.background:type_name -> hyapp.room.v1.RoomBackgroundImage + 0, // 64: hyapp.room.v1.ApplyRoomDecorationRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 65: hyapp.room.v1.ApplyRoomDecorationResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 66: hyapp.room.v1.ApplyRoomDecorationResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 63, // 67: hyapp.room.v1.ApplyRoomDecorationResponse.resource:type_name -> hyapp.room.v1.RoomDecorationResource + 0, // 68: hyapp.room.v1.AuthorizeRoomMediaUploadRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 55, // 69: hyapp.room.v1.AuthorizeRoomMediaUploadRequest.media:type_name -> hyapp.room.v1.RoomMedia + 0, // 70: hyapp.room.v1.CompleteRoomMediaUploadRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 55, // 71: hyapp.room.v1.CompleteRoomMediaUploadRequest.media:type_name -> hyapp.room.v1.RoomMedia + 55, // 72: hyapp.room.v1.CompleteRoomMediaUploadResponse.media:type_name -> hyapp.room.v1.RoomMedia + 0, // 73: hyapp.room.v1.SendRoomImageRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 55, // 74: hyapp.room.v1.SendRoomImageRequest.image:type_name -> hyapp.room.v1.RoomMedia + 1, // 75: hyapp.room.v1.SendRoomImageResponse.result:type_name -> hyapp.room.v1.CommandResult + 55, // 76: hyapp.room.v1.SendRoomImageResponse.image:type_name -> hyapp.room.v1.RoomMedia + 0, // 77: hyapp.room.v1.CreateRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 78: hyapp.room.v1.CreateRoomResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 79: hyapp.room.v1.CreateRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 80: hyapp.room.v1.UpdateRoomProfileRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 81: hyapp.room.v1.UpdateRoomProfileResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 82: hyapp.room.v1.UpdateRoomProfileResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 83: hyapp.room.v1.JoinRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 76, // 84: hyapp.room.v1.JoinRoomRequest.entry_vehicle:type_name -> hyapp.room.v1.RoomEntryVehicleSnapshot + 77, // 85: hyapp.room.v1.JoinRoomRequest.actor_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile + 1, // 86: hyapp.room.v1.JoinRoomResponse.result:type_name -> hyapp.room.v1.CommandResult + 2, // 87: hyapp.room.v1.JoinRoomResponse.user:type_name -> hyapp.room.v1.RoomUser + 54, // 88: hyapp.room.v1.JoinRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 79, // 89: hyapp.room.v1.JoinRoomResponse.effective_vip:type_name -> hyapp.room.v1.RoomEffectiveVipSnapshot + 0, // 90: hyapp.room.v1.RoomHeartbeatRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 91: hyapp.room.v1.RoomHeartbeatResponse.result:type_name -> hyapp.room.v1.CommandResult + 2, // 92: hyapp.room.v1.RoomHeartbeatResponse.user:type_name -> hyapp.room.v1.RoomUser + 54, // 93: hyapp.room.v1.RoomHeartbeatResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 94: hyapp.room.v1.LeaveRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 95: hyapp.room.v1.LeaveRoomResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 96: hyapp.room.v1.LeaveRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 97: hyapp.room.v1.CloseRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 98: hyapp.room.v1.CloseRoomResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 99: hyapp.room.v1.CloseRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 100: hyapp.room.v1.AdminListRoomsRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 87, // 101: hyapp.room.v1.AdminListRoomsResponse.rooms:type_name -> hyapp.room.v1.AdminRoomListItem + 0, // 102: hyapp.room.v1.AdminGetRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 87, // 103: hyapp.room.v1.AdminGetRoomResponse.room:type_name -> hyapp.room.v1.AdminRoomListItem + 0, // 104: hyapp.room.v1.AdminUpdateRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 105: hyapp.room.v1.AdminUpdateRoomResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 106: hyapp.room.v1.AdminUpdateRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 107: hyapp.room.v1.AdminDeleteRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 108: hyapp.room.v1.AdminDeleteRoomResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 109: hyapp.room.v1.AdminDeleteRoomResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 110: hyapp.room.v1.MicUpRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 77, // 111: hyapp.room.v1.MicUpRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile + 1, // 112: hyapp.room.v1.MicUpResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 113: hyapp.room.v1.MicUpResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 114: hyapp.room.v1.MicDownRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 77, // 115: hyapp.room.v1.MicDownRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile + 1, // 116: hyapp.room.v1.MicDownResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 117: hyapp.room.v1.MicDownResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 118: hyapp.room.v1.ChangeMicSeatRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 77, // 119: hyapp.room.v1.ChangeMicSeatRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile + 1, // 120: hyapp.room.v1.ChangeMicSeatResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 121: hyapp.room.v1.ChangeMicSeatResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 122: hyapp.room.v1.ConfirmMicPublishingRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 77, // 123: hyapp.room.v1.ConfirmMicPublishingRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile + 1, // 124: hyapp.room.v1.ConfirmMicPublishingResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 125: hyapp.room.v1.ConfirmMicPublishingResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 126: hyapp.room.v1.MicHeartbeatRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 127: hyapp.room.v1.MicHeartbeatResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 128: hyapp.room.v1.MicHeartbeatResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 129: hyapp.room.v1.SetMicMuteRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 77, // 130: hyapp.room.v1.SetMicMuteRequest.target_display_profile:type_name -> hyapp.room.v1.RoomUserDisplayProfile + 1, // 131: hyapp.room.v1.SetMicMuteResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 132: hyapp.room.v1.SetMicMuteResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 133: hyapp.room.v1.ApplyRTCEventRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 134: hyapp.room.v1.ApplyRTCEventResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 135: hyapp.room.v1.ApplyRTCEventResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 136: hyapp.room.v1.SetMicSeatLockRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 137: hyapp.room.v1.SetMicSeatLockResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 138: hyapp.room.v1.SetMicSeatLockResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 139: hyapp.room.v1.SetChatEnabledRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 140: hyapp.room.v1.SetChatEnabledResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 141: hyapp.room.v1.SetChatEnabledResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 142: hyapp.room.v1.SetRoomPasswordRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 143: hyapp.room.v1.SetRoomPasswordResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 144: hyapp.room.v1.SetRoomPasswordResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 145: hyapp.room.v1.SetRoomAdminRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 146: hyapp.room.v1.SetRoomAdminResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 147: hyapp.room.v1.SetRoomAdminResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 148: hyapp.room.v1.MuteUserRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 149: hyapp.room.v1.MuteUserResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 150: hyapp.room.v1.MuteUserResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 151: hyapp.room.v1.KickUserRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 152: hyapp.room.v1.KickUserResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 153: hyapp.room.v1.KickUserResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 154: hyapp.room.v1.UnbanUserRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 155: hyapp.room.v1.UnbanUserResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 156: hyapp.room.v1.UnbanUserResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 157: hyapp.room.v1.SystemEvictUserRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 1, // 158: hyapp.room.v1.SystemEvictUserResponse.result:type_name -> hyapp.room.v1.CommandResult + 54, // 159: hyapp.room.v1.SystemEvictUserResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 7, // 160: hyapp.room.v1.SendGiftBatchTarget.lucky_gift:type_name -> hyapp.room.v1.LuckyGiftDrawResult + 128, // 161: hyapp.room.v1.SendGiftBatchDisplay.targets:type_name -> hyapp.room.v1.SendGiftBatchTarget + 0, // 162: hyapp.room.v1.SendGiftRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 126, // 163: hyapp.room.v1.SendGiftRequest.target_host_scopes:type_name -> hyapp.room.v1.SendGiftTargetHostScope + 127, // 164: hyapp.room.v1.SendGiftRequest.sender_display_profile:type_name -> hyapp.room.v1.SendGiftDisplayProfile + 127, // 165: hyapp.room.v1.SendGiftRequest.target_display_profiles:type_name -> hyapp.room.v1.SendGiftDisplayProfile + 1, // 166: hyapp.room.v1.SendGiftResponse.result:type_name -> hyapp.room.v1.CommandResult + 5, // 167: hyapp.room.v1.SendGiftResponse.gift_rank:type_name -> hyapp.room.v1.RankItem + 54, // 168: hyapp.room.v1.SendGiftResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 13, // 169: hyapp.room.v1.SendGiftResponse.rocket:type_name -> hyapp.room.v1.RoomRocketState + 7, // 170: hyapp.room.v1.SendGiftResponse.lucky_gift:type_name -> hyapp.room.v1.LuckyGiftDrawResult + 7, // 171: hyapp.room.v1.SendGiftResponse.lucky_gifts:type_name -> hyapp.room.v1.LuckyGiftDrawResult + 129, // 172: hyapp.room.v1.SendGiftResponse.batch_display:type_name -> hyapp.room.v1.SendGiftBatchDisplay + 171, // 173: hyapp.room.v1.SendGiftResponse.v2:type_name -> hyapp.room.v1.SendGiftResultV2 + 0, // 174: hyapp.room.v1.ListRoomsRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 0, // 175: hyapp.room.v1.ListRoomsByOwnersRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 0, // 176: hyapp.room.v1.ListRoomFeedsRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 142, // 177: hyapp.room.v1.ListRoomFeedsRequest.related_users:type_name -> hyapp.room.v1.RoomFeedRelatedUser + 0, // 178: hyapp.room.v1.ListRoomGiftLeaderboardRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 63, // 179: hyapp.room.v1.RoomListItem.room_border:type_name -> hyapp.room.v1.RoomDecorationResource + 63, // 180: hyapp.room.v1.RoomListItem.room_name_style:type_name -> hyapp.room.v1.RoomDecorationResource + 143, // 181: hyapp.room.v1.ListRoomsResponse.rooms:type_name -> hyapp.room.v1.RoomListItem + 143, // 182: hyapp.room.v1.RoomGiftLeaderboardItem.room:type_name -> hyapp.room.v1.RoomListItem + 145, // 183: hyapp.room.v1.ListRoomGiftLeaderboardResponse.items:type_name -> hyapp.room.v1.RoomGiftLeaderboardItem + 0, // 184: hyapp.room.v1.ListRoomContributionRankRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 6, // 185: hyapp.room.v1.ListRoomContributionRankResponse.items:type_name -> hyapp.room.v1.RoomContributionRankItem + 0, // 186: hyapp.room.v1.GetMyRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 143, // 187: hyapp.room.v1.GetMyRoomResponse.room:type_name -> hyapp.room.v1.RoomListItem + 0, // 188: hyapp.room.v1.GetCurrentRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 0, // 189: hyapp.room.v1.BatchGetUserVoiceRoomPresencesRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 154, // 190: hyapp.room.v1.UserVoiceRoomPresence.room:type_name -> hyapp.room.v1.UserVoiceRoomPresenceRoom + 155, // 191: hyapp.room.v1.BatchGetUserVoiceRoomPresencesResponse.items:type_name -> hyapp.room.v1.UserVoiceRoomPresence + 0, // 192: hyapp.room.v1.GetRoomSnapshotRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 54, // 193: hyapp.room.v1.GetRoomSnapshotResponse.room:type_name -> hyapp.room.v1.RoomSnapshot + 0, // 194: hyapp.room.v1.GetRoomRocketRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 14, // 195: hyapp.room.v1.GetRoomRocketResponse.rocket:type_name -> hyapp.room.v1.RoomRocketInfo + 0, // 196: hyapp.room.v1.ListRoomOnlineUsersRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 2, // 197: hyapp.room.v1.ListRoomOnlineUsersResponse.users:type_name -> hyapp.room.v1.RoomUser + 3, // 198: hyapp.room.v1.ListRoomOnlineUsersResponse.items:type_name -> hyapp.room.v1.RoomOnlineUser + 0, // 199: hyapp.room.v1.ListRoomBannedUsersRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 163, // 200: hyapp.room.v1.ListRoomBannedUsersResponse.items:type_name -> hyapp.room.v1.RoomBannedUser + 0, // 201: hyapp.room.v1.FollowRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 0, // 202: hyapp.room.v1.UnfollowRoomRequest.meta:type_name -> hyapp.room.v1.RequestMeta + 7, // 203: hyapp.room.v1.SendGiftResultV2.lucky_gift:type_name -> hyapp.room.v1.LuckyGiftDrawResult + 7, // 204: hyapp.room.v1.SendGiftResultV2.lucky_gifts:type_name -> hyapp.room.v1.LuckyGiftDrawResult + 170, // 205: hyapp.room.v1.SendGiftResultV2.lucky_hits:type_name -> hyapp.room.v1.SendGiftLuckyHit + 129, // 206: hyapp.room.v1.SendGiftResultV2.batch_display:type_name -> hyapp.room.v1.SendGiftBatchDisplay + 5, // 207: hyapp.room.v1.SendGiftResultV2.gift_rank:type_name -> hyapp.room.v1.RankItem + 72, // 208: hyapp.room.v1.RoomCommandService.CreateRoom:input_type -> hyapp.room.v1.CreateRoomRequest + 74, // 209: hyapp.room.v1.RoomCommandService.UpdateRoomProfile:input_type -> hyapp.room.v1.UpdateRoomProfileRequest + 57, // 210: hyapp.room.v1.RoomCommandService.SaveRoomBackground:input_type -> hyapp.room.v1.SaveRoomBackgroundRequest + 61, // 211: hyapp.room.v1.RoomCommandService.SetRoomBackground:input_type -> hyapp.room.v1.SetRoomBackgroundRequest + 64, // 212: hyapp.room.v1.RoomCommandService.ApplyRoomDecoration:input_type -> hyapp.room.v1.ApplyRoomDecorationRequest + 70, // 213: hyapp.room.v1.RoomCommandService.SendRoomImage:input_type -> hyapp.room.v1.SendRoomImageRequest + 78, // 214: hyapp.room.v1.RoomCommandService.JoinRoom:input_type -> hyapp.room.v1.JoinRoomRequest + 81, // 215: hyapp.room.v1.RoomCommandService.RoomHeartbeat:input_type -> hyapp.room.v1.RoomHeartbeatRequest + 83, // 216: hyapp.room.v1.RoomCommandService.LeaveRoom:input_type -> hyapp.room.v1.LeaveRoomRequest + 85, // 217: hyapp.room.v1.RoomCommandService.CloseRoom:input_type -> hyapp.room.v1.CloseRoomRequest + 92, // 218: hyapp.room.v1.RoomCommandService.AdminUpdateRoom:input_type -> hyapp.room.v1.AdminUpdateRoomRequest + 94, // 219: hyapp.room.v1.RoomCommandService.AdminDeleteRoom:input_type -> hyapp.room.v1.AdminDeleteRoomRequest + 19, // 220: hyapp.room.v1.RoomCommandService.AdminUpdateRoomRocketConfig:input_type -> hyapp.room.v1.AdminUpdateRoomRocketConfigRequest + 24, // 221: hyapp.room.v1.RoomCommandService.AdminUpdateRoomSeatConfig:input_type -> hyapp.room.v1.AdminUpdateRoomSeatConfigRequest + 31, // 222: hyapp.room.v1.RoomCommandService.AdminUpdateHumanRoomRobotConfig:input_type -> hyapp.room.v1.AdminUpdateHumanRoomRobotConfigRequest + 37, // 223: hyapp.room.v1.RoomCommandService.AdminCreateRoomPin:input_type -> hyapp.room.v1.AdminCreateRoomPinRequest + 39, // 224: hyapp.room.v1.RoomCommandService.AdminCancelRoomPin:input_type -> hyapp.room.v1.AdminCancelRoomPinRequest + 45, // 225: hyapp.room.v1.RoomCommandService.AdminCreateRobotRoom:input_type -> hyapp.room.v1.AdminCreateRobotRoomRequest + 47, // 226: hyapp.room.v1.RoomCommandService.AdminSetRobotRoomStatus:input_type -> hyapp.room.v1.AdminSetRobotRoomStatusRequest + 49, // 227: hyapp.room.v1.RoomCommandService.AdminRenameOwnerCountryCode:input_type -> hyapp.room.v1.AdminRenameOwnerCountryCodeRequest + 96, // 228: hyapp.room.v1.RoomCommandService.MicUp:input_type -> hyapp.room.v1.MicUpRequest + 98, // 229: hyapp.room.v1.RoomCommandService.MicDown:input_type -> hyapp.room.v1.MicDownRequest + 100, // 230: hyapp.room.v1.RoomCommandService.ChangeMicSeat:input_type -> hyapp.room.v1.ChangeMicSeatRequest + 102, // 231: hyapp.room.v1.RoomCommandService.ConfirmMicPublishing:input_type -> hyapp.room.v1.ConfirmMicPublishingRequest + 104, // 232: hyapp.room.v1.RoomCommandService.MicHeartbeat:input_type -> hyapp.room.v1.MicHeartbeatRequest + 106, // 233: hyapp.room.v1.RoomCommandService.SetMicMute:input_type -> hyapp.room.v1.SetMicMuteRequest + 108, // 234: hyapp.room.v1.RoomCommandService.ApplyRTCEvent:input_type -> hyapp.room.v1.ApplyRTCEventRequest + 110, // 235: hyapp.room.v1.RoomCommandService.SetMicSeatLock:input_type -> hyapp.room.v1.SetMicSeatLockRequest + 112, // 236: hyapp.room.v1.RoomCommandService.SetChatEnabled:input_type -> hyapp.room.v1.SetChatEnabledRequest + 114, // 237: hyapp.room.v1.RoomCommandService.SetRoomPassword:input_type -> hyapp.room.v1.SetRoomPasswordRequest + 116, // 238: hyapp.room.v1.RoomCommandService.SetRoomAdmin:input_type -> hyapp.room.v1.SetRoomAdminRequest + 118, // 239: hyapp.room.v1.RoomCommandService.MuteUser:input_type -> hyapp.room.v1.MuteUserRequest + 120, // 240: hyapp.room.v1.RoomCommandService.KickUser:input_type -> hyapp.room.v1.KickUserRequest + 122, // 241: hyapp.room.v1.RoomCommandService.UnbanUser:input_type -> hyapp.room.v1.UnbanUserRequest + 124, // 242: hyapp.room.v1.RoomCommandService.SystemEvictUser:input_type -> hyapp.room.v1.SystemEvictUserRequest + 130, // 243: hyapp.room.v1.RoomCommandService.SendGift:input_type -> hyapp.room.v1.SendGiftRequest + 166, // 244: hyapp.room.v1.RoomCommandService.FollowRoom:input_type -> hyapp.room.v1.FollowRoomRequest + 168, // 245: hyapp.room.v1.RoomCommandService.UnfollowRoom:input_type -> hyapp.room.v1.UnfollowRoomRequest + 132, // 246: hyapp.room.v1.RoomGuardService.CheckSpeakPermission:input_type -> hyapp.room.v1.CheckSpeakPermissionRequest + 134, // 247: hyapp.room.v1.RoomGuardService.VerifyRoomPresence:input_type -> hyapp.room.v1.VerifyRoomPresenceRequest + 136, // 248: hyapp.room.v1.RoomGuardService.ResolveRoomAppCode:input_type -> hyapp.room.v1.ResolveRoomAppCodeRequest + 66, // 249: hyapp.room.v1.RoomGuardService.AuthorizeRoomMediaUpload:input_type -> hyapp.room.v1.AuthorizeRoomMediaUploadRequest + 68, // 250: hyapp.room.v1.RoomGuardService.CompleteRoomMediaUpload:input_type -> hyapp.room.v1.CompleteRoomMediaUploadRequest + 88, // 251: hyapp.room.v1.RoomQueryService.AdminListRooms:input_type -> hyapp.room.v1.AdminListRoomsRequest + 90, // 252: hyapp.room.v1.RoomQueryService.AdminGetRoom:input_type -> hyapp.room.v1.AdminGetRoomRequest + 17, // 253: hyapp.room.v1.RoomQueryService.AdminGetRoomRocketConfig:input_type -> hyapp.room.v1.AdminGetRoomRocketConfigRequest + 22, // 254: hyapp.room.v1.RoomQueryService.AdminGetRoomSeatConfig:input_type -> hyapp.room.v1.AdminGetRoomSeatConfigRequest + 29, // 255: hyapp.room.v1.RoomQueryService.AdminGetHumanRoomRobotConfig:input_type -> hyapp.room.v1.AdminGetHumanRoomRobotConfigRequest + 35, // 256: hyapp.room.v1.RoomQueryService.AdminListRoomPins:input_type -> hyapp.room.v1.AdminListRoomPinsRequest + 43, // 257: hyapp.room.v1.RoomQueryService.AdminListRobotRooms:input_type -> hyapp.room.v1.AdminListRobotRoomsRequest + 51, // 258: hyapp.room.v1.RoomQueryService.AdminFilterAvailableRoomRobots:input_type -> hyapp.room.v1.AdminFilterAvailableRoomRobotsRequest + 138, // 259: hyapp.room.v1.RoomQueryService.ListRooms:input_type -> hyapp.room.v1.ListRoomsRequest + 139, // 260: hyapp.room.v1.RoomQueryService.ListRoomsByOwners:input_type -> hyapp.room.v1.ListRoomsByOwnersRequest + 140, // 261: hyapp.room.v1.RoomQueryService.ListRoomFeeds:input_type -> hyapp.room.v1.ListRoomFeedsRequest + 141, // 262: hyapp.room.v1.RoomQueryService.ListRoomGiftLeaderboard:input_type -> hyapp.room.v1.ListRoomGiftLeaderboardRequest + 147, // 263: hyapp.room.v1.RoomQueryService.ListRoomContributionRank:input_type -> hyapp.room.v1.ListRoomContributionRankRequest + 149, // 264: hyapp.room.v1.RoomQueryService.GetMyRoom:input_type -> hyapp.room.v1.GetMyRoomRequest + 151, // 265: hyapp.room.v1.RoomQueryService.GetCurrentRoom:input_type -> hyapp.room.v1.GetCurrentRoomRequest + 153, // 266: hyapp.room.v1.RoomQueryService.BatchGetUserVoiceRoomPresences:input_type -> hyapp.room.v1.BatchGetUserVoiceRoomPresencesRequest + 157, // 267: hyapp.room.v1.RoomQueryService.GetRoomSnapshot:input_type -> hyapp.room.v1.GetRoomSnapshotRequest + 59, // 268: hyapp.room.v1.RoomQueryService.ListRoomBackgrounds:input_type -> hyapp.room.v1.ListRoomBackgroundsRequest + 159, // 269: hyapp.room.v1.RoomQueryService.GetRoomRocket:input_type -> hyapp.room.v1.GetRoomRocketRequest + 161, // 270: hyapp.room.v1.RoomQueryService.ListRoomOnlineUsers:input_type -> hyapp.room.v1.ListRoomOnlineUsersRequest + 164, // 271: hyapp.room.v1.RoomQueryService.ListRoomBannedUsers:input_type -> hyapp.room.v1.ListRoomBannedUsersRequest + 73, // 272: hyapp.room.v1.RoomCommandService.CreateRoom:output_type -> hyapp.room.v1.CreateRoomResponse + 75, // 273: hyapp.room.v1.RoomCommandService.UpdateRoomProfile:output_type -> hyapp.room.v1.UpdateRoomProfileResponse + 58, // 274: hyapp.room.v1.RoomCommandService.SaveRoomBackground:output_type -> hyapp.room.v1.SaveRoomBackgroundResponse + 62, // 275: hyapp.room.v1.RoomCommandService.SetRoomBackground:output_type -> hyapp.room.v1.SetRoomBackgroundResponse + 65, // 276: hyapp.room.v1.RoomCommandService.ApplyRoomDecoration:output_type -> hyapp.room.v1.ApplyRoomDecorationResponse + 71, // 277: hyapp.room.v1.RoomCommandService.SendRoomImage:output_type -> hyapp.room.v1.SendRoomImageResponse + 80, // 278: hyapp.room.v1.RoomCommandService.JoinRoom:output_type -> hyapp.room.v1.JoinRoomResponse + 82, // 279: hyapp.room.v1.RoomCommandService.RoomHeartbeat:output_type -> hyapp.room.v1.RoomHeartbeatResponse + 84, // 280: hyapp.room.v1.RoomCommandService.LeaveRoom:output_type -> hyapp.room.v1.LeaveRoomResponse + 86, // 281: hyapp.room.v1.RoomCommandService.CloseRoom:output_type -> hyapp.room.v1.CloseRoomResponse + 93, // 282: hyapp.room.v1.RoomCommandService.AdminUpdateRoom:output_type -> hyapp.room.v1.AdminUpdateRoomResponse + 95, // 283: hyapp.room.v1.RoomCommandService.AdminDeleteRoom:output_type -> hyapp.room.v1.AdminDeleteRoomResponse + 20, // 284: hyapp.room.v1.RoomCommandService.AdminUpdateRoomRocketConfig:output_type -> hyapp.room.v1.AdminUpdateRoomRocketConfigResponse + 25, // 285: hyapp.room.v1.RoomCommandService.AdminUpdateRoomSeatConfig:output_type -> hyapp.room.v1.AdminUpdateRoomSeatConfigResponse + 32, // 286: hyapp.room.v1.RoomCommandService.AdminUpdateHumanRoomRobotConfig:output_type -> hyapp.room.v1.AdminUpdateHumanRoomRobotConfigResponse + 38, // 287: hyapp.room.v1.RoomCommandService.AdminCreateRoomPin:output_type -> hyapp.room.v1.AdminCreateRoomPinResponse + 40, // 288: hyapp.room.v1.RoomCommandService.AdminCancelRoomPin:output_type -> hyapp.room.v1.AdminCancelRoomPinResponse + 46, // 289: hyapp.room.v1.RoomCommandService.AdminCreateRobotRoom:output_type -> hyapp.room.v1.AdminCreateRobotRoomResponse + 48, // 290: hyapp.room.v1.RoomCommandService.AdminSetRobotRoomStatus:output_type -> hyapp.room.v1.AdminSetRobotRoomStatusResponse + 50, // 291: hyapp.room.v1.RoomCommandService.AdminRenameOwnerCountryCode:output_type -> hyapp.room.v1.AdminRenameOwnerCountryCodeResponse + 97, // 292: hyapp.room.v1.RoomCommandService.MicUp:output_type -> hyapp.room.v1.MicUpResponse + 99, // 293: hyapp.room.v1.RoomCommandService.MicDown:output_type -> hyapp.room.v1.MicDownResponse + 101, // 294: hyapp.room.v1.RoomCommandService.ChangeMicSeat:output_type -> hyapp.room.v1.ChangeMicSeatResponse + 103, // 295: hyapp.room.v1.RoomCommandService.ConfirmMicPublishing:output_type -> hyapp.room.v1.ConfirmMicPublishingResponse + 105, // 296: hyapp.room.v1.RoomCommandService.MicHeartbeat:output_type -> hyapp.room.v1.MicHeartbeatResponse + 107, // 297: hyapp.room.v1.RoomCommandService.SetMicMute:output_type -> hyapp.room.v1.SetMicMuteResponse + 109, // 298: hyapp.room.v1.RoomCommandService.ApplyRTCEvent:output_type -> hyapp.room.v1.ApplyRTCEventResponse + 111, // 299: hyapp.room.v1.RoomCommandService.SetMicSeatLock:output_type -> hyapp.room.v1.SetMicSeatLockResponse + 113, // 300: hyapp.room.v1.RoomCommandService.SetChatEnabled:output_type -> hyapp.room.v1.SetChatEnabledResponse + 115, // 301: hyapp.room.v1.RoomCommandService.SetRoomPassword:output_type -> hyapp.room.v1.SetRoomPasswordResponse + 117, // 302: hyapp.room.v1.RoomCommandService.SetRoomAdmin:output_type -> hyapp.room.v1.SetRoomAdminResponse + 119, // 303: hyapp.room.v1.RoomCommandService.MuteUser:output_type -> hyapp.room.v1.MuteUserResponse + 121, // 304: hyapp.room.v1.RoomCommandService.KickUser:output_type -> hyapp.room.v1.KickUserResponse + 123, // 305: hyapp.room.v1.RoomCommandService.UnbanUser:output_type -> hyapp.room.v1.UnbanUserResponse + 125, // 306: hyapp.room.v1.RoomCommandService.SystemEvictUser:output_type -> hyapp.room.v1.SystemEvictUserResponse + 131, // 307: hyapp.room.v1.RoomCommandService.SendGift:output_type -> hyapp.room.v1.SendGiftResponse + 167, // 308: hyapp.room.v1.RoomCommandService.FollowRoom:output_type -> hyapp.room.v1.FollowRoomResponse + 169, // 309: hyapp.room.v1.RoomCommandService.UnfollowRoom:output_type -> hyapp.room.v1.UnfollowRoomResponse + 133, // 310: hyapp.room.v1.RoomGuardService.CheckSpeakPermission:output_type -> hyapp.room.v1.CheckSpeakPermissionResponse + 135, // 311: hyapp.room.v1.RoomGuardService.VerifyRoomPresence:output_type -> hyapp.room.v1.VerifyRoomPresenceResponse + 137, // 312: hyapp.room.v1.RoomGuardService.ResolveRoomAppCode:output_type -> hyapp.room.v1.ResolveRoomAppCodeResponse + 67, // 313: hyapp.room.v1.RoomGuardService.AuthorizeRoomMediaUpload:output_type -> hyapp.room.v1.AuthorizeRoomMediaUploadResponse + 69, // 314: hyapp.room.v1.RoomGuardService.CompleteRoomMediaUpload:output_type -> hyapp.room.v1.CompleteRoomMediaUploadResponse + 89, // 315: hyapp.room.v1.RoomQueryService.AdminListRooms:output_type -> hyapp.room.v1.AdminListRoomsResponse + 91, // 316: hyapp.room.v1.RoomQueryService.AdminGetRoom:output_type -> hyapp.room.v1.AdminGetRoomResponse + 18, // 317: hyapp.room.v1.RoomQueryService.AdminGetRoomRocketConfig:output_type -> hyapp.room.v1.AdminGetRoomRocketConfigResponse + 23, // 318: hyapp.room.v1.RoomQueryService.AdminGetRoomSeatConfig:output_type -> hyapp.room.v1.AdminGetRoomSeatConfigResponse + 30, // 319: hyapp.room.v1.RoomQueryService.AdminGetHumanRoomRobotConfig:output_type -> hyapp.room.v1.AdminGetHumanRoomRobotConfigResponse + 36, // 320: hyapp.room.v1.RoomQueryService.AdminListRoomPins:output_type -> hyapp.room.v1.AdminListRoomPinsResponse + 44, // 321: hyapp.room.v1.RoomQueryService.AdminListRobotRooms:output_type -> hyapp.room.v1.AdminListRobotRoomsResponse + 52, // 322: hyapp.room.v1.RoomQueryService.AdminFilterAvailableRoomRobots:output_type -> hyapp.room.v1.AdminFilterAvailableRoomRobotsResponse + 144, // 323: hyapp.room.v1.RoomQueryService.ListRooms:output_type -> hyapp.room.v1.ListRoomsResponse + 144, // 324: hyapp.room.v1.RoomQueryService.ListRoomsByOwners:output_type -> hyapp.room.v1.ListRoomsResponse + 144, // 325: hyapp.room.v1.RoomQueryService.ListRoomFeeds:output_type -> hyapp.room.v1.ListRoomsResponse + 146, // 326: hyapp.room.v1.RoomQueryService.ListRoomGiftLeaderboard:output_type -> hyapp.room.v1.ListRoomGiftLeaderboardResponse + 148, // 327: hyapp.room.v1.RoomQueryService.ListRoomContributionRank:output_type -> hyapp.room.v1.ListRoomContributionRankResponse + 150, // 328: hyapp.room.v1.RoomQueryService.GetMyRoom:output_type -> hyapp.room.v1.GetMyRoomResponse + 152, // 329: hyapp.room.v1.RoomQueryService.GetCurrentRoom:output_type -> hyapp.room.v1.GetCurrentRoomResponse + 156, // 330: hyapp.room.v1.RoomQueryService.BatchGetUserVoiceRoomPresences:output_type -> hyapp.room.v1.BatchGetUserVoiceRoomPresencesResponse + 158, // 331: hyapp.room.v1.RoomQueryService.GetRoomSnapshot:output_type -> hyapp.room.v1.GetRoomSnapshotResponse + 60, // 332: hyapp.room.v1.RoomQueryService.ListRoomBackgrounds:output_type -> hyapp.room.v1.ListRoomBackgroundsResponse + 160, // 333: hyapp.room.v1.RoomQueryService.GetRoomRocket:output_type -> hyapp.room.v1.GetRoomRocketResponse + 162, // 334: hyapp.room.v1.RoomQueryService.ListRoomOnlineUsers:output_type -> hyapp.room.v1.ListRoomOnlineUsersResponse + 165, // 335: hyapp.room.v1.RoomQueryService.ListRoomBannedUsers:output_type -> hyapp.room.v1.ListRoomBannedUsersResponse + 272, // [272:336] is the sub-list for method output_type + 208, // [208:272] is the sub-list for method input_type + 208, // [208:208] is the sub-list for extension type_name + 208, // [208:208] is the sub-list for extension extendee + 0, // [0:208] is the sub-list for field type_name } func init() { file_proto_room_v1_room_proto_init() } @@ -14272,15 +15699,15 @@ func file_proto_room_v1_room_proto_init() { if File_proto_room_v1_room_proto != nil { return } - file_proto_room_v1_room_proto_msgTypes[64].OneofWrappers = []any{} - file_proto_room_v1_room_proto_msgTypes[82].OneofWrappers = []any{} + file_proto_room_v1_room_proto_msgTypes[74].OneofWrappers = []any{} + file_proto_room_v1_room_proto_msgTypes[92].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_room_v1_room_proto_rawDesc), len(file_proto_room_v1_room_proto_rawDesc)), NumEnums: 0, - NumMessages: 159, + NumMessages: 173, NumExtensions: 0, NumServices: 3, }, diff --git a/api/proto/room/v1/room.proto b/api/proto/room/v1/room.proto index f530360d..b0fcc438 100644 --- a/api/proto/room/v1/room.proto +++ b/api/proto/room/v1/room.proto @@ -552,6 +552,30 @@ message RoomSnapshot { repeated RoomBanState ban_states = 21; // online_count 允许 lite 响应不携带 online_users 明细仍保留准确在线人数。 int32 online_count = 22; + // room_border 与 room_name_style 是房间级最终装扮快照;客户端不得再按房主 VIP 等级推导样式。 + RoomDecorationResource room_border = 23; + RoomDecorationResource room_name_style = 24; + // active_background 固化当前背景的真实媒体属性;room_ext.background_url 仅保留旧客户端兼容。 + RoomBackgroundImage active_background = 25; +} + +// RoomMedia 是房间背景和房间图片消息共同使用的可信媒体快照。 +// URL 只能由 gateway 的受信对象存储上传入口生成;room-service 会再次校验 object_key 归属和媒体边界。 +message RoomMedia { + string url = 1; + string object_key = 2; + string content_type = 3; + string format = 4; + int64 size_bytes = 5; + int32 width = 6; + int32 height = 7; + bool animated = 8; + int32 frame_count = 9; + int64 duration_ms = 10; + string poster_url = 11; + string thumbnail_url = 12; + string sha256 = 13; + string status = 14; } // RoomBackgroundImage 是房间维度保存过的背景图素材。 @@ -563,12 +587,16 @@ message RoomBackgroundImage { int64 created_by_user_id = 4; int64 created_at_ms = 5; int64 updated_at_ms = 6; + RoomMedia media = 7; + string command_id = 8; } message SaveRoomBackgroundRequest { RequestMeta meta = 1; string room_id = 2; + // image_url 仅兼容旧内部调用;新版专用上传必须传完整 media,保存时不按 URL 后缀猜格式。 string image_url = 3; + RoomMedia media = 4; } message SaveRoomBackgroundResponse { @@ -586,6 +614,7 @@ message ListRoomBackgroundsResponse { repeated RoomBackgroundImage backgrounds = 1; string selected_background_url = 2; int64 server_time_ms = 3; + RoomBackgroundImage selected_background = 4; } message SetRoomBackgroundRequest { @@ -599,6 +628,85 @@ message SetRoomBackgroundResponse { RoomBackgroundImage background = 3; } +// RoomDecorationResource 是 wallet 资源目录和有效 entitlement 在房间应用时固化的展示快照。 +// 资源后续编辑、下架或 VIP 到期不会篡改历史 command log;新的读取只返回当前 Room Cell 已应用结果。 +message RoomDecorationResource { + int64 resource_id = 1; + string resource_code = 2; + string resource_type = 3; + string name = 4; + string asset_url = 5; + string preview_url = 6; + string animation_url = 7; + string format = 8; + string metadata_json = 9; + string entitlement_id = 10; + repeated string text_colors = 11; + repeated double stops = 12; + double angle_degrees = 13; + int64 expires_at_ms = 14; +} + +message ApplyRoomDecorationRequest { + RequestMeta meta = 1; + // decoration_type 仅接受 room_border/colored_room_name;资源类型分别固定为 room_border/room_name_style。 + string decoration_type = 2; + int64 resource_id = 3; + string entitlement_id = 4; +} + +message ApplyRoomDecorationResponse { + CommandResult result = 1; + RoomSnapshot room = 2; + string decoration_type = 3; + RoomDecorationResource resource = 4; + int64 server_time_ms = 5; +} + +message AuthorizeRoomMediaUploadRequest { + RequestMeta meta = 1; + // purpose 仅接受 room_background/room_image;授权发生在 COS PutObject 之前。 + string purpose = 2; + RoomMedia media = 3; +} + +message AuthorizeRoomMediaUploadResponse { + bool allowed = 1; + string object_key_prefix = 2; + int64 room_version = 3; + int64 server_time_ms = 4; + // object_key 由 room owner 按首次 command_id+媒体哈希确定,gateway 不能自行换 key。 + string object_key = 5; +} + +message CompleteRoomMediaUploadRequest { + RequestMeta meta = 1; + string purpose = 2; + // media 必须携带 PutObject 成功后的 url/object_key,并与首次 Authorize 的解析事实完全一致。 + RoomMedia media = 3; +} + +message CompleteRoomMediaUploadResponse { + bool active = 1; + RoomMedia media = 2; + int64 server_time_ms = 3; +} + +message SendRoomImageRequest { + RequestMeta meta = 1; + RoomMedia image = 2; +} + +message SendRoomImageResponse { + CommandResult result = 1; + bool accepted = 2; + string message_id = 3; + string room_id = 4; + int64 sender_user_id = 5; + int64 server_time_ms = 6; + RoomMedia image = 7; +} + // CreateRoomRequest 创建一个新的房间执行单元。 // 必填语义:meta.room_id、meta.command_id、meta.actor_user_id、mode、room_name 和 room_avatar。 // 同一个 meta.actor_user_id 只能作为 owner 创建一个房间;重复创建返回 Conflict。 @@ -1309,6 +1417,8 @@ message RoomListItem { string room_short_id = 14; bool locked = 15; string country_code = 16; + RoomDecorationResource room_border = 17; + RoomDecorationResource room_name_style = 18; } // ListRoomsResponse 返回一页房间卡片和下一页不透明 cursor。 @@ -1391,6 +1501,47 @@ message GetCurrentRoomResponse { int64 server_time_ms = 9; } +// BatchGetUserVoiceRoomPresencesRequest 查询一组用户当前可向 viewer 展示的上麦入口。 +// user_ids 只能由 gateway 从外部查询参数解析;room-service 仍会去重并限制最多 100 个。 +message BatchGetUserVoiceRoomPresencesRequest { + RequestMeta meta = 1; + int64 viewer_user_id = 2; + repeated int64 user_ids = 3; +} + +// UserVoiceRoomPresenceRoom 是个人资料页 Live 入口需要的最小房间投影。 +// 它不携带在线用户和完整麦位集合,避免 batch 查询把 RoomSnapshot 明细扩散到用户域接口。 +message UserVoiceRoomPresenceRoom { + string room_id = 1; + string room_short_id = 2; + int64 owner_user_id = 3; + // 当前房间模型没有独立主持人字段,host_user_id 与 owner_user_id 保持一致。 + int64 host_user_id = 4; + string title = 5; + string cover_url = 6; + string background_url = 7; + string mode = 8; + int32 online_count = 9; + int32 seat_count = 10; + bool locked = 11; +} + +// UserVoiceRoomPresence 对每个去重后的请求用户返回一条稳定结果。 +// 不可见、不可进入和未上麦统一返回 not_on_mic,避免通过差异状态探测房间位置。 +message UserVoiceRoomPresence { + int64 user_id = 1; + string state = 2; + int32 seat_no = 3; + bool joinable = 4; + int64 observed_at_ms = 5; + UserVoiceRoomPresenceRoom room = 6; +} + +message BatchGetUserVoiceRoomPresencesResponse { + repeated UserVoiceRoomPresence items = 1; + int64 server_time_ms = 2; +} + // GetRoomSnapshotRequest 主动读取当前房间完整快照。 // 该查询必须由 gateway 写入鉴权后的 viewer_user_id,客户端不能提交 viewer_user_id。 message GetRoomSnapshotRequest { @@ -1501,6 +1652,8 @@ service RoomCommandService { rpc UpdateRoomProfile(UpdateRoomProfileRequest) returns (UpdateRoomProfileResponse); rpc SaveRoomBackground(SaveRoomBackgroundRequest) returns (SaveRoomBackgroundResponse); rpc SetRoomBackground(SetRoomBackgroundRequest) returns (SetRoomBackgroundResponse); + rpc ApplyRoomDecoration(ApplyRoomDecorationRequest) returns (ApplyRoomDecorationResponse); + rpc SendRoomImage(SendRoomImageRequest) returns (SendRoomImageResponse); rpc JoinRoom(JoinRoomRequest) returns (JoinRoomResponse); rpc RoomHeartbeat(RoomHeartbeatRequest) returns (RoomHeartbeatResponse); rpc LeaveRoom(LeaveRoomRequest) returns (LeaveRoomResponse); @@ -1540,6 +1693,8 @@ service RoomGuardService { rpc CheckSpeakPermission(CheckSpeakPermissionRequest) returns (CheckSpeakPermissionResponse); rpc VerifyRoomPresence(VerifyRoomPresenceRequest) returns (VerifyRoomPresenceResponse); rpc ResolveRoomAppCode(ResolveRoomAppCodeRequest) returns (ResolveRoomAppCodeResponse); + rpc AuthorizeRoomMediaUpload(AuthorizeRoomMediaUploadRequest) returns (AuthorizeRoomMediaUploadResponse); + rpc CompleteRoomMediaUpload(CompleteRoomMediaUploadRequest) returns (CompleteRoomMediaUploadResponse); } // RoomQueryService 承载不会改变 Room Cell 状态的读模型查询。 @@ -1559,6 +1714,7 @@ service RoomQueryService { rpc ListRoomContributionRank(ListRoomContributionRankRequest) returns (ListRoomContributionRankResponse); rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse); rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse); + rpc BatchGetUserVoiceRoomPresences(BatchGetUserVoiceRoomPresencesRequest) returns (BatchGetUserVoiceRoomPresencesResponse); rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse); rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse); rpc GetRoomRocket(GetRoomRocketRequest) returns (GetRoomRocketResponse); diff --git a/api/proto/room/v1/room_grpc.pb.go b/api/proto/room/v1/room_grpc.pb.go index fcc21dab..e7cf7647 100644 --- a/api/proto/room/v1/room_grpc.pb.go +++ b/api/proto/room/v1/room_grpc.pb.go @@ -23,6 +23,8 @@ const ( RoomCommandService_UpdateRoomProfile_FullMethodName = "/hyapp.room.v1.RoomCommandService/UpdateRoomProfile" RoomCommandService_SaveRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SaveRoomBackground" RoomCommandService_SetRoomBackground_FullMethodName = "/hyapp.room.v1.RoomCommandService/SetRoomBackground" + RoomCommandService_ApplyRoomDecoration_FullMethodName = "/hyapp.room.v1.RoomCommandService/ApplyRoomDecoration" + RoomCommandService_SendRoomImage_FullMethodName = "/hyapp.room.v1.RoomCommandService/SendRoomImage" RoomCommandService_JoinRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/JoinRoom" RoomCommandService_RoomHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/RoomHeartbeat" RoomCommandService_LeaveRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/LeaveRoom" @@ -67,6 +69,8 @@ type RoomCommandServiceClient interface { UpdateRoomProfile(ctx context.Context, in *UpdateRoomProfileRequest, opts ...grpc.CallOption) (*UpdateRoomProfileResponse, error) SaveRoomBackground(ctx context.Context, in *SaveRoomBackgroundRequest, opts ...grpc.CallOption) (*SaveRoomBackgroundResponse, error) SetRoomBackground(ctx context.Context, in *SetRoomBackgroundRequest, opts ...grpc.CallOption) (*SetRoomBackgroundResponse, error) + ApplyRoomDecoration(ctx context.Context, in *ApplyRoomDecorationRequest, opts ...grpc.CallOption) (*ApplyRoomDecorationResponse, error) + SendRoomImage(ctx context.Context, in *SendRoomImageRequest, opts ...grpc.CallOption) (*SendRoomImageResponse, error) JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error) RoomHeartbeat(ctx context.Context, in *RoomHeartbeatRequest, opts ...grpc.CallOption) (*RoomHeartbeatResponse, error) LeaveRoom(ctx context.Context, in *LeaveRoomRequest, opts ...grpc.CallOption) (*LeaveRoomResponse, error) @@ -149,6 +153,26 @@ func (c *roomCommandServiceClient) SetRoomBackground(ctx context.Context, in *Se return out, nil } +func (c *roomCommandServiceClient) ApplyRoomDecoration(ctx context.Context, in *ApplyRoomDecorationRequest, opts ...grpc.CallOption) (*ApplyRoomDecorationResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ApplyRoomDecorationResponse) + err := c.cc.Invoke(ctx, RoomCommandService_ApplyRoomDecoration_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *roomCommandServiceClient) SendRoomImage(ctx context.Context, in *SendRoomImageRequest, opts ...grpc.CallOption) (*SendRoomImageResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SendRoomImageResponse) + err := c.cc.Invoke(ctx, RoomCommandService_SendRoomImage_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *roomCommandServiceClient) JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(JoinRoomResponse) @@ -479,6 +503,8 @@ type RoomCommandServiceServer interface { UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error) SaveRoomBackground(context.Context, *SaveRoomBackgroundRequest) (*SaveRoomBackgroundResponse, error) SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error) + ApplyRoomDecoration(context.Context, *ApplyRoomDecorationRequest) (*ApplyRoomDecorationResponse, error) + SendRoomImage(context.Context, *SendRoomImageRequest) (*SendRoomImageResponse, error) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error) LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, error) @@ -533,6 +559,12 @@ func (UnimplementedRoomCommandServiceServer) SaveRoomBackground(context.Context, func (UnimplementedRoomCommandServiceServer) SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error) { return nil, status.Error(codes.Unimplemented, "method SetRoomBackground not implemented") } +func (UnimplementedRoomCommandServiceServer) ApplyRoomDecoration(context.Context, *ApplyRoomDecorationRequest) (*ApplyRoomDecorationResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ApplyRoomDecoration not implemented") +} +func (UnimplementedRoomCommandServiceServer) SendRoomImage(context.Context, *SendRoomImageRequest) (*SendRoomImageResponse, error) { + return nil, status.Error(codes.Unimplemented, "method SendRoomImage not implemented") +} func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) { return nil, status.Error(codes.Unimplemented, "method JoinRoom not implemented") } @@ -722,6 +754,42 @@ func _RoomCommandService_SetRoomBackground_Handler(srv interface{}, ctx context. return interceptor(ctx, in, info, handler) } +func _RoomCommandService_ApplyRoomDecoration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ApplyRoomDecorationRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoomCommandServiceServer).ApplyRoomDecoration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoomCommandService_ApplyRoomDecoration_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoomCommandServiceServer).ApplyRoomDecoration(ctx, req.(*ApplyRoomDecorationRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoomCommandService_SendRoomImage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendRoomImageRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoomCommandServiceServer).SendRoomImage(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoomCommandService_SendRoomImage_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoomCommandServiceServer).SendRoomImage(ctx, req.(*SendRoomImageRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _RoomCommandService_JoinRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(JoinRoomRequest) if err := dec(in); err != nil { @@ -1321,6 +1389,14 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{ MethodName: "SetRoomBackground", Handler: _RoomCommandService_SetRoomBackground_Handler, }, + { + MethodName: "ApplyRoomDecoration", + Handler: _RoomCommandService_ApplyRoomDecoration_Handler, + }, + { + MethodName: "SendRoomImage", + Handler: _RoomCommandService_SendRoomImage_Handler, + }, { MethodName: "JoinRoom", Handler: _RoomCommandService_JoinRoom_Handler, @@ -1455,9 +1531,11 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{ } const ( - RoomGuardService_CheckSpeakPermission_FullMethodName = "/hyapp.room.v1.RoomGuardService/CheckSpeakPermission" - RoomGuardService_VerifyRoomPresence_FullMethodName = "/hyapp.room.v1.RoomGuardService/VerifyRoomPresence" - RoomGuardService_ResolveRoomAppCode_FullMethodName = "/hyapp.room.v1.RoomGuardService/ResolveRoomAppCode" + RoomGuardService_CheckSpeakPermission_FullMethodName = "/hyapp.room.v1.RoomGuardService/CheckSpeakPermission" + RoomGuardService_VerifyRoomPresence_FullMethodName = "/hyapp.room.v1.RoomGuardService/VerifyRoomPresence" + RoomGuardService_ResolveRoomAppCode_FullMethodName = "/hyapp.room.v1.RoomGuardService/ResolveRoomAppCode" + RoomGuardService_AuthorizeRoomMediaUpload_FullMethodName = "/hyapp.room.v1.RoomGuardService/AuthorizeRoomMediaUpload" + RoomGuardService_CompleteRoomMediaUpload_FullMethodName = "/hyapp.room.v1.RoomGuardService/CompleteRoomMediaUpload" ) // RoomGuardServiceClient is the client API for RoomGuardService service. @@ -1469,6 +1547,8 @@ type RoomGuardServiceClient interface { CheckSpeakPermission(ctx context.Context, in *CheckSpeakPermissionRequest, opts ...grpc.CallOption) (*CheckSpeakPermissionResponse, error) VerifyRoomPresence(ctx context.Context, in *VerifyRoomPresenceRequest, opts ...grpc.CallOption) (*VerifyRoomPresenceResponse, error) ResolveRoomAppCode(ctx context.Context, in *ResolveRoomAppCodeRequest, opts ...grpc.CallOption) (*ResolveRoomAppCodeResponse, error) + AuthorizeRoomMediaUpload(ctx context.Context, in *AuthorizeRoomMediaUploadRequest, opts ...grpc.CallOption) (*AuthorizeRoomMediaUploadResponse, error) + CompleteRoomMediaUpload(ctx context.Context, in *CompleteRoomMediaUploadRequest, opts ...grpc.CallOption) (*CompleteRoomMediaUploadResponse, error) } type roomGuardServiceClient struct { @@ -1509,6 +1589,26 @@ func (c *roomGuardServiceClient) ResolveRoomAppCode(ctx context.Context, in *Res return out, nil } +func (c *roomGuardServiceClient) AuthorizeRoomMediaUpload(ctx context.Context, in *AuthorizeRoomMediaUploadRequest, opts ...grpc.CallOption) (*AuthorizeRoomMediaUploadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthorizeRoomMediaUploadResponse) + err := c.cc.Invoke(ctx, RoomGuardService_AuthorizeRoomMediaUpload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *roomGuardServiceClient) CompleteRoomMediaUpload(ctx context.Context, in *CompleteRoomMediaUploadRequest, opts ...grpc.CallOption) (*CompleteRoomMediaUploadResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CompleteRoomMediaUploadResponse) + err := c.cc.Invoke(ctx, RoomGuardService_CompleteRoomMediaUpload_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // RoomGuardServiceServer is the server API for RoomGuardService service. // All implementations must embed UnimplementedRoomGuardServiceServer // for forward compatibility. @@ -1518,6 +1618,8 @@ type RoomGuardServiceServer interface { CheckSpeakPermission(context.Context, *CheckSpeakPermissionRequest) (*CheckSpeakPermissionResponse, error) VerifyRoomPresence(context.Context, *VerifyRoomPresenceRequest) (*VerifyRoomPresenceResponse, error) ResolveRoomAppCode(context.Context, *ResolveRoomAppCodeRequest) (*ResolveRoomAppCodeResponse, error) + AuthorizeRoomMediaUpload(context.Context, *AuthorizeRoomMediaUploadRequest) (*AuthorizeRoomMediaUploadResponse, error) + CompleteRoomMediaUpload(context.Context, *CompleteRoomMediaUploadRequest) (*CompleteRoomMediaUploadResponse, error) mustEmbedUnimplementedRoomGuardServiceServer() } @@ -1537,6 +1639,12 @@ func (UnimplementedRoomGuardServiceServer) VerifyRoomPresence(context.Context, * func (UnimplementedRoomGuardServiceServer) ResolveRoomAppCode(context.Context, *ResolveRoomAppCodeRequest) (*ResolveRoomAppCodeResponse, error) { return nil, status.Error(codes.Unimplemented, "method ResolveRoomAppCode not implemented") } +func (UnimplementedRoomGuardServiceServer) AuthorizeRoomMediaUpload(context.Context, *AuthorizeRoomMediaUploadRequest) (*AuthorizeRoomMediaUploadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method AuthorizeRoomMediaUpload not implemented") +} +func (UnimplementedRoomGuardServiceServer) CompleteRoomMediaUpload(context.Context, *CompleteRoomMediaUploadRequest) (*CompleteRoomMediaUploadResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CompleteRoomMediaUpload not implemented") +} func (UnimplementedRoomGuardServiceServer) mustEmbedUnimplementedRoomGuardServiceServer() {} func (UnimplementedRoomGuardServiceServer) testEmbeddedByValue() {} @@ -1612,6 +1720,42 @@ func _RoomGuardService_ResolveRoomAppCode_Handler(srv interface{}, ctx context.C return interceptor(ctx, in, info, handler) } +func _RoomGuardService_AuthorizeRoomMediaUpload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthorizeRoomMediaUploadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoomGuardServiceServer).AuthorizeRoomMediaUpload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoomGuardService_AuthorizeRoomMediaUpload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoomGuardServiceServer).AuthorizeRoomMediaUpload(ctx, req.(*AuthorizeRoomMediaUploadRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RoomGuardService_CompleteRoomMediaUpload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CompleteRoomMediaUploadRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoomGuardServiceServer).CompleteRoomMediaUpload(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoomGuardService_CompleteRoomMediaUpload_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoomGuardServiceServer).CompleteRoomMediaUpload(ctx, req.(*CompleteRoomMediaUploadRequest)) + } + return interceptor(ctx, in, info, handler) +} + // RoomGuardService_ServiceDesc is the grpc.ServiceDesc for RoomGuardService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -1631,6 +1775,14 @@ var RoomGuardService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ResolveRoomAppCode", Handler: _RoomGuardService_ResolveRoomAppCode_Handler, }, + { + MethodName: "AuthorizeRoomMediaUpload", + Handler: _RoomGuardService_AuthorizeRoomMediaUpload_Handler, + }, + { + MethodName: "CompleteRoomMediaUpload", + Handler: _RoomGuardService_CompleteRoomMediaUpload_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "proto/room/v1/room.proto", @@ -1652,6 +1804,7 @@ const ( RoomQueryService_ListRoomContributionRank_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomContributionRank" RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom" RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom" + RoomQueryService_BatchGetUserVoiceRoomPresences_FullMethodName = "/hyapp.room.v1.RoomQueryService/BatchGetUserVoiceRoomPresences" RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot" RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds" RoomQueryService_GetRoomRocket_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomRocket" @@ -1680,6 +1833,7 @@ type RoomQueryServiceClient interface { ListRoomContributionRank(ctx context.Context, in *ListRoomContributionRankRequest, opts ...grpc.CallOption) (*ListRoomContributionRankResponse, error) GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error) GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error) + BatchGetUserVoiceRoomPresences(ctx context.Context, in *BatchGetUserVoiceRoomPresencesRequest, opts ...grpc.CallOption) (*BatchGetUserVoiceRoomPresencesResponse, error) GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error) ListRoomBackgrounds(ctx context.Context, in *ListRoomBackgroundsRequest, opts ...grpc.CallOption) (*ListRoomBackgroundsResponse, error) GetRoomRocket(ctx context.Context, in *GetRoomRocketRequest, opts ...grpc.CallOption) (*GetRoomRocketResponse, error) @@ -1845,6 +1999,16 @@ func (c *roomQueryServiceClient) GetCurrentRoom(ctx context.Context, in *GetCurr return out, nil } +func (c *roomQueryServiceClient) BatchGetUserVoiceRoomPresences(ctx context.Context, in *BatchGetUserVoiceRoomPresencesRequest, opts ...grpc.CallOption) (*BatchGetUserVoiceRoomPresencesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(BatchGetUserVoiceRoomPresencesResponse) + err := c.cc.Invoke(ctx, RoomQueryService_BatchGetUserVoiceRoomPresences_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *roomQueryServiceClient) GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetRoomSnapshotResponse) @@ -1916,6 +2080,7 @@ type RoomQueryServiceServer interface { ListRoomContributionRank(context.Context, *ListRoomContributionRankRequest) (*ListRoomContributionRankResponse, error) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) + BatchGetUserVoiceRoomPresences(context.Context, *BatchGetUserVoiceRoomPresencesRequest) (*BatchGetUserVoiceRoomPresencesResponse, error) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error) GetRoomRocket(context.Context, *GetRoomRocketRequest) (*GetRoomRocketResponse, error) @@ -1976,6 +2141,9 @@ func (UnimplementedRoomQueryServiceServer) GetMyRoom(context.Context, *GetMyRoom func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetCurrentRoom not implemented") } +func (UnimplementedRoomQueryServiceServer) BatchGetUserVoiceRoomPresences(context.Context, *BatchGetUserVoiceRoomPresencesRequest) (*BatchGetUserVoiceRoomPresencesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method BatchGetUserVoiceRoomPresences not implemented") +} func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) { return nil, status.Error(codes.Unimplemented, "method GetRoomSnapshot not implemented") } @@ -2282,6 +2450,24 @@ func _RoomQueryService_GetCurrentRoom_Handler(srv interface{}, ctx context.Conte return interceptor(ctx, in, info, handler) } +func _RoomQueryService_BatchGetUserVoiceRoomPresences_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BatchGetUserVoiceRoomPresencesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RoomQueryServiceServer).BatchGetUserVoiceRoomPresences(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RoomQueryService_BatchGetUserVoiceRoomPresences_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RoomQueryServiceServer).BatchGetUserVoiceRoomPresences(ctx, req.(*BatchGetUserVoiceRoomPresencesRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _RoomQueryService_GetRoomSnapshot_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetRoomSnapshotRequest) if err := dec(in); err != nil { @@ -2439,6 +2625,10 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetCurrentRoom", Handler: _RoomQueryService_GetCurrentRoom_Handler, }, + { + MethodName: "BatchGetUserVoiceRoomPresences", + Handler: _RoomQueryService_BatchGetUserVoiceRoomPresences_Handler, + }, { MethodName: "GetRoomSnapshot", Handler: _RoomQueryService_GetRoomSnapshot_Handler, diff --git a/api/proto/user/v1/user.pb.go b/api/proto/user/v1/user.pb.go index 90594456..6c98fa37 100644 --- a/api/proto/user/v1/user.pb.go +++ b/api/proto/user/v1/user.pb.go @@ -2211,11 +2211,14 @@ func (x *RecordProfileVisitRequest) GetTargetUserId() int64 { } type RecordProfileVisitResponse struct { - state protoimpl.MessageState `protogen:"open.v1"` - Recorded bool `protobuf:"varint,1,opt,name=recorded,proto3" json:"recorded,omitempty"` - TargetStats *UserProfileStats `protobuf:"bytes,2,opt,name=target_stats,json=targetStats,proto3" json:"target_stats,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Recorded bool `protobuf:"varint,1,opt,name=recorded,proto3" json:"recorded,omitempty"` + TargetStats *UserProfileStats `protobuf:"bytes,2,opt,name=target_stats,json=targetStats,proto3" json:"target_stats,omitempty"` + // target_stats_hidden=true 时 target_stats 必须为空;该标志由 user-service + // 基于目标用户当前 hide_profile_data 权益实时判定,客户端不能自行推断。 + TargetStatsHidden bool `protobuf:"varint,3,opt,name=target_stats_hidden,json=targetStatsHidden,proto3" json:"target_stats_hidden,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *RecordProfileVisitResponse) Reset() { @@ -2262,14 +2265,27 @@ func (x *RecordProfileVisitResponse) GetTargetStats() *UserProfileStats { return nil } +func (x *RecordProfileVisitResponse) GetTargetStatsHidden() bool { + if x != nil { + return x.TargetStatsHidden + } + return false +} + type ProfileVisitRecord struct { - state protoimpl.MessageState `protogen:"open.v1"` - VisitorUserId int64 `protobuf:"varint,1,opt,name=visitor_user_id,json=visitorUserId,proto3" json:"visitor_user_id,omitempty"` - TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"` - VisitCount int64 `protobuf:"varint,3,opt,name=visit_count,json=visitCount,proto3" json:"visit_count,omitempty"` - LastVisitedAtMs int64 `protobuf:"varint,4,opt,name=last_visited_at_ms,json=lastVisitedAtMs,proto3" json:"last_visited_at_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + // 隐藏访客身份的记录不返回真实 visitor_user_id,字段保持 0;客户端必须使用 + // visitor_identity_hidden/anonymous_visit_id 分支渲染匿名访客,不能把 0 当作用户 ID。 + VisitorUserId int64 `protobuf:"varint,1,opt,name=visitor_user_id,json=visitorUserId,proto3" json:"visitor_user_id,omitempty"` + TargetUserId int64 `protobuf:"varint,2,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"` + VisitCount int64 `protobuf:"varint,3,opt,name=visit_count,json=visitCount,proto3" json:"visit_count,omitempty"` + LastVisitedAtMs int64 `protobuf:"varint,4,opt,name=last_visited_at_ms,json=lastVisitedAtMs,proto3" json:"last_visited_at_ms,omitempty"` + VisitorIdentityHidden bool `protobuf:"varint,5,opt,name=visitor_identity_hidden,json=visitorIdentityHidden,proto3" json:"visitor_identity_hidden,omitempty"` + // anonymous_visit_id 是首次匿名访问时随机生成、仅在同一 target 下稳定的展示 ID, + // 不包含真实 user_id,也不能用于查询用户资料。 + AnonymousVisitId string `protobuf:"bytes,6,opt,name=anonymous_visit_id,json=anonymousVisitId,proto3" json:"anonymous_visit_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ProfileVisitRecord) Reset() { @@ -2330,6 +2346,20 @@ func (x *ProfileVisitRecord) GetLastVisitedAtMs() int64 { return 0 } +func (x *ProfileVisitRecord) GetVisitorIdentityHidden() bool { + if x != nil { + return x.VisitorIdentityHidden + } + return false +} + +func (x *ProfileVisitRecord) GetAnonymousVisitId() string { + if x != nil { + return x.AnonymousVisitId + } + return "" +} + type ListProfileVisitorsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` @@ -13650,16 +13680,19 @@ const file_proto_user_v1_user_proto_rawDesc = "" + "\x19RecordProfileVisitRequest\x12.\n" + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12&\n" + "\x0fvisitor_user_id\x18\x02 \x01(\x03R\rvisitorUserId\x12$\n" + - "\x0etarget_user_id\x18\x03 \x01(\x03R\ftargetUserId\"|\n" + + "\x0etarget_user_id\x18\x03 \x01(\x03R\ftargetUserId\"\xac\x01\n" + "\x1aRecordProfileVisitResponse\x12\x1a\n" + "\brecorded\x18\x01 \x01(\bR\brecorded\x12B\n" + - "\ftarget_stats\x18\x02 \x01(\v2\x1f.hyapp.user.v1.UserProfileStatsR\vtargetStats\"\xb0\x01\n" + + "\ftarget_stats\x18\x02 \x01(\v2\x1f.hyapp.user.v1.UserProfileStatsR\vtargetStats\x12.\n" + + "\x13target_stats_hidden\x18\x03 \x01(\bR\x11targetStatsHidden\"\x96\x02\n" + "\x12ProfileVisitRecord\x12&\n" + "\x0fvisitor_user_id\x18\x01 \x01(\x03R\rvisitorUserId\x12$\n" + "\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x1f\n" + "\vvisit_count\x18\x03 \x01(\x03R\n" + "visitCount\x12+\n" + - "\x12last_visited_at_ms\x18\x04 \x01(\x03R\x0flastVisitedAtMs\"\x96\x01\n" + + "\x12last_visited_at_ms\x18\x04 \x01(\x03R\x0flastVisitedAtMs\x126\n" + + "\x17visitor_identity_hidden\x18\x05 \x01(\bR\x15visitorIdentityHidden\x12,\n" + + "\x12anonymous_visit_id\x18\x06 \x01(\tR\x10anonymousVisitId\"\x96\x01\n" + "\x1aListProfileVisitorsRequest\x12.\n" + "\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x17\n" + "\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x12\n" + diff --git a/api/proto/user/v1/user.proto b/api/proto/user/v1/user.proto index 759f781c..e74ee8e4 100644 --- a/api/proto/user/v1/user.proto +++ b/api/proto/user/v1/user.proto @@ -262,13 +262,22 @@ message RecordProfileVisitRequest { message RecordProfileVisitResponse { bool recorded = 1; UserProfileStats target_stats = 2; + // target_stats_hidden=true 时 target_stats 必须为空;该标志由 user-service + // 基于目标用户当前 hide_profile_data 权益实时判定,客户端不能自行推断。 + bool target_stats_hidden = 3; } message ProfileVisitRecord { + // 隐藏访客身份的记录不返回真实 visitor_user_id,字段保持 0;客户端必须使用 + // visitor_identity_hidden/anonymous_visit_id 分支渲染匿名访客,不能把 0 当作用户 ID。 int64 visitor_user_id = 1; int64 target_user_id = 2; int64 visit_count = 3; int64 last_visited_at_ms = 4; + bool visitor_identity_hidden = 5; + // anonymous_visit_id 是首次匿名访问时随机生成、仅在同一 target 下稳定的展示 ID, + // 不包含真实 user_id,也不能用于查询用户资料。 + string anonymous_visit_id = 6; } message ListProfileVisitorsRequest { diff --git a/api/proto/wallet/v1/wallet.pb.go b/api/proto/wallet/v1/wallet.pb.go index 6d14f257..9a10d601 100644 --- a/api/proto/wallet/v1/wallet.pb.go +++ b/api/proto/wallet/v1/wallet.pb.go @@ -9665,6 +9665,8 @@ type EquipUserResourceRequest struct { UserId int64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` ResourceId int64 `protobuf:"varint,4,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"` EntitlementId string `protobuf:"bytes,5,opt,name=entitlement_id,json=entitlementId,proto3" json:"entitlement_id,omitempty"` + // command_id 是装备状态变更的业务幂等键;request_id 只用于链路追踪。 + CommandId string `protobuf:"bytes,6,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -9734,6 +9736,13 @@ func (x *EquipUserResourceRequest) GetEntitlementId() string { return "" } +func (x *EquipUserResourceRequest) GetCommandId() string { + if x != nil { + return x.CommandId + } + return "" +} + type EquipUserResourceResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Resource *UserResourceEntitlement `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` @@ -17461,8 +17470,11 @@ type VipBenefit struct { CreatedAtMs int64 `protobuf:"varint,13,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"` UpdatedAtMs int64 `protobuf:"varint,14,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"` Presentation *VipBenefitPresentation `protobuf:"bytes,15,opt,name=presentation,proto3" json:"presentation,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // resource 仅在 resource_id 指向同 App、active 且 resource_type 匹配的真实资源时返回。 + // 运营尚未配置素材的权益保留 resource_id/resource_type 事实,但该对象为空,客户端不得猜 URL。 + Resource *Resource `protobuf:"bytes,16,opt,name=resource,proto3" json:"resource,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *VipBenefit) Reset() { @@ -17600,6 +17612,13 @@ func (x *VipBenefit) GetPresentation() *VipBenefitPresentation { return nil } +func (x *VipBenefit) GetResource() *Resource { + if x != nil { + return x.Resource + } + return nil +} + type VipLevel struct { state protoimpl.MessageState `protogen:"open.v1"` Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"` @@ -20856,8 +20875,12 @@ type CheckVipBenefitResponse struct { State *VipState `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"` DenialReason string `protobuf:"bytes,4,opt,name=denial_reason,json=denialReason,proto3" json:"denial_reason,omitempty"` EvaluatedAtMs int64 `protobuf:"varint,5,opt,name=evaluated_at_ms,json=evaluatedAtMs,proto3" json:"evaluated_at_ms,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // required_level 是当前 App 所有 active 等级中首次显式配置该权益的等级;0 表示未配置。 + RequiredLevel int32 `protobuf:"varint,6,opt,name=required_level,json=requiredLevel,proto3" json:"required_level,omitempty"` + // current_effective_level 来自 wallet 合并付费会员与体验卡后的最终状态。 + CurrentEffectiveLevel int32 `protobuf:"varint,7,opt,name=current_effective_level,json=currentEffectiveLevel,proto3" json:"current_effective_level,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *CheckVipBenefitResponse) Reset() { @@ -20925,6 +20948,20 @@ func (x *CheckVipBenefitResponse) GetEvaluatedAtMs() int64 { return 0 } +func (x *CheckVipBenefitResponse) GetRequiredLevel() int32 { + if x != nil { + return x.RequiredLevel + } + return 0 +} + +func (x *CheckVipBenefitResponse) GetCurrentEffectiveLevel() int32 { + if x != nil { + return x.CurrentEffectiveLevel + } + return 0 +} + type AdminVipLevelInput struct { state protoimpl.MessageState `protogen:"open.v1"` Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"` @@ -27280,7 +27317,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\vactive_only\x18\x05 \x01(\bR\n" + "activeOnly\"c\n" + "\x19ListUserResourcesResponse\x12F\n" + - "\tresources\x18\x01 \x03(\v2(.hyapp.wallet.v1.UserResourceEntitlementR\tresources\"\xb5\x01\n" + + "\tresources\x18\x01 \x03(\v2(.hyapp.wallet.v1.UserResourceEntitlementR\tresources\"\xd4\x01\n" + "\x18EquipUserResourceRequest\x12\x1d\n" + "\n" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" + @@ -27288,7 +27325,9 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\auser_id\x18\x03 \x01(\x03R\x06userId\x12\x1f\n" + "\vresource_id\x18\x04 \x01(\x03R\n" + "resourceId\x12%\n" + - "\x0eentitlement_id\x18\x05 \x01(\tR\rentitlementId\"a\n" + + "\x0eentitlement_id\x18\x05 \x01(\tR\rentitlementId\x12\x1d\n" + + "\n" + + "command_id\x18\x06 \x01(\tR\tcommandId\"a\n" + "\x19EquipUserResourceResponse\x12D\n" + "\bresource\x18\x01 \x01(\v2(.hyapp.wallet.v1.UserResourceEntitlementR\bresource\"\x94\x01\n" + "\x1aUnequipUserResourceRequest\x12\x1d\n" + @@ -28055,7 +28094,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\x0econfig_version\x18\v \x01(\x03R\rconfigVersion\x12-\n" + "\x13updated_by_admin_id\x18\f \x01(\x03R\x10updatedByAdminId\x12\"\n" + "\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12\"\n" + - "\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\"\xad\x04\n" + + "\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\"\xe4\x04\n" + "\n" + "VipBenefit\x12!\n" + "\fbenefit_code\x18\x01 \x01(\tR\vbenefitCode\x12\x12\n" + @@ -28076,7 +28115,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "\rmetadata_json\x18\f \x01(\tR\fmetadataJson\x12\"\n" + "\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12\"\n" + "\rupdated_at_ms\x18\x0e \x01(\x03R\vupdatedAtMs\x12K\n" + - "\fpresentation\x18\x0f \x01(\v2'.hyapp.wallet.v1.VipBenefitPresentationR\fpresentation\"\xdc\x05\n" + + "\fpresentation\x18\x0f \x01(\v2'.hyapp.wallet.v1.VipBenefitPresentationR\fpresentation\x125\n" + + "\bresource\x18\x10 \x01(\v2\x19.hyapp.wallet.v1.ResourceR\bresource\"\xdc\x05\n" + "\bVipLevel\x12\x14\n" + "\x05level\x18\x01 \x01(\x05R\x05level\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + @@ -28388,13 +28428,15 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" + "request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" + "\bapp_code\x18\x02 \x01(\tR\aappCode\x12\x17\n" + "\auser_id\x18\x03 \x01(\x03R\x06userId\x12!\n" + - "\fbenefit_code\x18\x04 \x01(\tR\vbenefitCode\"\xe8\x01\n" + + "\fbenefit_code\x18\x04 \x01(\tR\vbenefitCode\"\xc7\x02\n" + "\x17CheckVipBenefitResponse\x12\x18\n" + "\aallowed\x18\x01 \x01(\bR\aallowed\x125\n" + "\abenefit\x18\x02 \x01(\v2\x1b.hyapp.wallet.v1.VipBenefitR\abenefit\x12/\n" + "\x05state\x18\x03 \x01(\v2\x19.hyapp.wallet.v1.VipStateR\x05state\x12#\n" + "\rdenial_reason\x18\x04 \x01(\tR\fdenialReason\x12&\n" + - "\x0fevaluated_at_ms\x18\x05 \x01(\x03R\revaluatedAtMs\"\xea\x02\n" + + "\x0fevaluated_at_ms\x18\x05 \x01(\x03R\revaluatedAtMs\x12%\n" + + "\x0erequired_level\x18\x06 \x01(\x05R\rrequiredLevel\x126\n" + + "\x17current_effective_level\x18\a \x01(\x05R\x15currentEffectiveLevel\"\xea\x02\n" + "\x12AdminVipLevelInput\x12\x14\n" + "\x05level\x18\x01 \x01(\x05R\x05level\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" + @@ -29434,343 +29476,344 @@ var file_proto_wallet_v1_wallet_proto_depIdxs = []int32{ 184, // 83: hyapp.wallet.v1.GetDiamondExchangeConfigResponse.rules:type_name -> hyapp.wallet.v1.DiamondExchangeRule 187, // 84: hyapp.wallet.v1.ListWalletTransactionsResponse.transactions:type_name -> hyapp.wallet.v1.WalletTransaction 297, // 85: hyapp.wallet.v1.VipBenefit.presentation:type_name -> hyapp.wallet.v1.VipBenefitPresentation - 190, // 86: hyapp.wallet.v1.VipLevel.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem - 192, // 87: hyapp.wallet.v1.VipLevel.benefits:type_name -> hyapp.wallet.v1.VipBenefit - 194, // 88: hyapp.wallet.v1.VipState.paid_vip:type_name -> hyapp.wallet.v1.UserVip - 195, // 89: hyapp.wallet.v1.VipState.equipped_trial_card:type_name -> hyapp.wallet.v1.VipTrialCard - 194, // 90: hyapp.wallet.v1.VipState.effective_vip:type_name -> hyapp.wallet.v1.UserVip - 192, // 91: hyapp.wallet.v1.VipState.effective_benefits:type_name -> hyapp.wallet.v1.VipBenefit - 191, // 92: hyapp.wallet.v1.VipState.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig - 196, // 93: hyapp.wallet.v1.VipState.user_settings:type_name -> hyapp.wallet.v1.VipUserSettings - 194, // 94: hyapp.wallet.v1.ListVipPackagesResponse.current_vip:type_name -> hyapp.wallet.v1.UserVip - 193, // 95: hyapp.wallet.v1.ListVipPackagesResponse.packages:type_name -> hyapp.wallet.v1.VipLevel - 197, // 96: hyapp.wallet.v1.ListVipPackagesResponse.state:type_name -> hyapp.wallet.v1.VipState - 191, // 97: hyapp.wallet.v1.ListVipPackagesResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig - 194, // 98: hyapp.wallet.v1.GetMyVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip - 197, // 99: hyapp.wallet.v1.GetMyVipResponse.state:type_name -> hyapp.wallet.v1.VipState - 194, // 100: hyapp.wallet.v1.PurchaseVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip - 190, // 101: hyapp.wallet.v1.PurchaseVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem - 197, // 102: hyapp.wallet.v1.PurchaseVipResponse.state:type_name -> hyapp.wallet.v1.VipState - 8, // 103: hyapp.wallet.v1.PurchaseVipResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 104: hyapp.wallet.v1.DebitCPBreakupFeeResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 105: hyapp.wallet.v1.DebitWheelDrawResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 194, // 106: hyapp.wallet.v1.GrantVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip - 190, // 107: hyapp.wallet.v1.GrantVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem - 197, // 108: hyapp.wallet.v1.GrantVipResponse.state:type_name -> hyapp.wallet.v1.VipState - 191, // 109: hyapp.wallet.v1.GetVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig - 192, // 110: hyapp.wallet.v1.GetVipProgramConfigResponse.benefits:type_name -> hyapp.wallet.v1.VipBenefit - 191, // 111: hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest.config:type_name -> hyapp.wallet.v1.VipProgramConfig - 191, // 112: hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig - 196, // 113: hyapp.wallet.v1.UpdateMyVipSettingsResponse.settings:type_name -> hyapp.wallet.v1.VipUserSettings - 216, // 114: hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse.run:type_name -> hyapp.wallet.v1.VipDailyCoinRebateRun - 217, // 115: hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate - 217, // 116: hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse.rebates:type_name -> hyapp.wallet.v1.VipDailyCoinRebate - 217, // 117: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate - 8, // 118: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance - 195, // 119: hyapp.wallet.v1.GrantVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard - 197, // 120: hyapp.wallet.v1.GrantVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState - 195, // 121: hyapp.wallet.v1.EquipVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard - 197, // 122: hyapp.wallet.v1.EquipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState - 197, // 123: hyapp.wallet.v1.UnequipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState - 192, // 124: hyapp.wallet.v1.CheckVipBenefitResponse.benefit:type_name -> hyapp.wallet.v1.VipBenefit - 197, // 125: hyapp.wallet.v1.CheckVipBenefitResponse.state:type_name -> hyapp.wallet.v1.VipState - 192, // 126: hyapp.wallet.v1.AdminVipLevelInput.benefits:type_name -> hyapp.wallet.v1.VipBenefit - 193, // 127: hyapp.wallet.v1.ListAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel - 191, // 128: hyapp.wallet.v1.ListAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig - 234, // 129: hyapp.wallet.v1.UpdateAdminVipLevelsRequest.levels:type_name -> hyapp.wallet.v1.AdminVipLevelInput - 193, // 130: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel - 191, // 131: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig - 8, // 132: hyapp.wallet.v1.CreditTaskRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 133: hyapp.wallet.v1.CreditLuckyGiftRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 134: hyapp.wallet.v1.CreditWheelRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 135: hyapp.wallet.v1.CreditRoomTurnoverRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 136: hyapp.wallet.v1.CreditInviteActivityRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 8, // 137: hyapp.wallet.v1.CreditAgencyOpeningRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance - 254, // 138: hyapp.wallet.v1.RedPacket.claims:type_name -> hyapp.wallet.v1.RedPacketClaim - 253, // 139: hyapp.wallet.v1.GetRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig - 253, // 140: hyapp.wallet.v1.UpdateRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig - 255, // 141: hyapp.wallet.v1.CreateRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket - 254, // 142: hyapp.wallet.v1.ClaimRedPacketResponse.claim:type_name -> hyapp.wallet.v1.RedPacketClaim - 255, // 143: hyapp.wallet.v1.ClaimRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket - 255, // 144: hyapp.wallet.v1.ListRedPacketsResponse.packets:type_name -> hyapp.wallet.v1.RedPacket - 255, // 145: hyapp.wallet.v1.GetRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket - 255, // 146: hyapp.wallet.v1.RetryRedPacketRefundResponse.packet:type_name -> hyapp.wallet.v1.RedPacket - 48, // 147: hyapp.wallet.v1.ResourceShopPurchaseOrder.resource:type_name -> hyapp.wallet.v1.Resource - 274, // 148: hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse.orders:type_name -> hyapp.wallet.v1.ResourceShopPurchaseOrder - 279, // 149: hyapp.wallet.v1.GetHostRevenueStatsResponse.stats:type_name -> hyapp.wallet.v1.HostRevenueStats - 282, // 150: hyapp.wallet.v1.GetAgencyPointShareStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyPointShareStats - 285, // 151: hyapp.wallet.v1.GetAgencyHostGiftStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyHostGiftStats - 288, // 152: hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse.sellers:type_name -> hyapp.wallet.v1.PointWithdrawalCoinSellerConfig - 295, // 153: hyapp.wallet.v1.VipBenefitPresentation.preview_items:type_name -> hyapp.wallet.v1.VipBenefitPreviewItem - 296, // 154: hyapp.wallet.v1.VipBenefitPresentation.numeric_reward:type_name -> hyapp.wallet.v1.VipBenefitNumericReward - 57, // 155: hyapp.wallet.v1.RevokeUserResourceResponse.resource:type_name -> hyapp.wallet.v1.UserResourceEntitlement - 272, // 156: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest - 272, // 157: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest - 272, // 158: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:input_type -> hyapp.wallet.v1.CronBatchRequest - 218, // 159: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:input_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchRequest - 0, // 160: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest - 1, // 161: hyapp.wallet.v1.WalletService.DebitDirectGift:input_type -> hyapp.wallet.v1.DebitDirectGiftRequest - 4, // 162: hyapp.wallet.v1.WalletService.BatchDebitGift:input_type -> hyapp.wallet.v1.BatchDebitGiftRequest - 7, // 163: hyapp.wallet.v1.WalletService.DebitRobotGift:input_type -> hyapp.wallet.v1.DebitRobotGiftRequest - 9, // 164: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest - 13, // 165: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:input_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyRequest - 16, // 166: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:input_type -> hyapp.wallet.v1.GetHostSalaryProgressRequest - 280, // 167: hyapp.wallet.v1.WalletService.GetHostRevenueStats:input_type -> hyapp.wallet.v1.GetHostRevenueStatsRequest - 283, // 168: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:input_type -> hyapp.wallet.v1.GetAgencyPointShareStatsRequest - 286, // 169: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:input_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsRequest - 19, // 170: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:input_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsRequest - 21, // 171: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest - 23, // 172: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest - 25, // 173: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest - 27, // 174: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:input_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildRequest - 30, // 175: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:input_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersRequest - 32, // 176: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:input_type -> hyapp.wallet.v1.ExchangeSalaryToCoinRequest - 34, // 177: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:input_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerRequest - 289, // 178: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:input_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersRequest - 291, // 179: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:input_type -> hyapp.wallet.v1.TransferPointToCoinSellerRequest - 293, // 180: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:input_type -> hyapp.wallet.v1.GetPointWithdrawalConfigRequest - 36, // 181: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:input_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalRequest - 38, // 182: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:input_type -> hyapp.wallet.v1.SettleSalaryWithdrawalRequest - 40, // 183: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:input_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalRequest - 42, // 184: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:input_type -> hyapp.wallet.v1.FreezePointWithdrawalRequest - 44, // 185: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:input_type -> hyapp.wallet.v1.SettlePointWithdrawalRequest - 46, // 186: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:input_type -> hyapp.wallet.v1.ReleasePointWithdrawalRequest - 61, // 187: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest - 63, // 188: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest - 65, // 189: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest - 66, // 190: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest - 67, // 191: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest - 68, // 192: hyapp.wallet.v1.WalletService.DeleteResource:input_type -> hyapp.wallet.v1.DeleteResourceRequest - 69, // 193: hyapp.wallet.v1.WalletService.BatchDeleteResources:input_type -> hyapp.wallet.v1.BatchDeleteResourcesRequest - 72, // 194: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest - 74, // 195: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest - 76, // 196: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest - 77, // 197: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest - 78, // 198: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest - 52, // 199: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:input_type -> hyapp.wallet.v1.PinResourceGroupSnapshotRequest - 80, // 200: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest - 82, // 201: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:input_type -> hyapp.wallet.v1.ListGiftTypeConfigsRequest - 277, // 202: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:input_type -> hyapp.wallet.v1.GetGiftCatalogVersionRequest - 86, // 203: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest - 87, // 204: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:input_type -> hyapp.wallet.v1.BatchCreateGiftConfigsRequest - 89, // 205: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest - 90, // 206: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest - 91, // 207: hyapp.wallet.v1.WalletService.DeleteGiftConfig:input_type -> hyapp.wallet.v1.DeleteGiftConfigRequest - 84, // 208: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:input_type -> hyapp.wallet.v1.UpsertGiftTypeConfigRequest - 93, // 209: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest - 94, // 210: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest - 95, // 211: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:input_type -> hyapp.wallet.v1.GrantPinnedResourceGroupRequest - 96, // 212: hyapp.wallet.v1.WalletService.RevokeResourceGrant:input_type -> hyapp.wallet.v1.RevokeResourceGrantRequest - 298, // 213: hyapp.wallet.v1.WalletService.RevokeUserResource:input_type -> hyapp.wallet.v1.RevokeUserResourceRequest - 98, // 214: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest - 100, // 215: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest - 102, // 216: hyapp.wallet.v1.WalletService.UnequipUserResource:input_type -> hyapp.wallet.v1.UnequipUserResourceRequest - 104, // 217: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:input_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesRequest - 107, // 218: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest - 110, // 219: hyapp.wallet.v1.WalletService.ListResourceShopItems:input_type -> hyapp.wallet.v1.ListResourceShopItemsRequest - 112, // 220: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:input_type -> hyapp.wallet.v1.UpsertResourceShopItemsRequest - 114, // 221: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:input_type -> hyapp.wallet.v1.SetResourceShopItemStatusRequest - 275, // 222: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:input_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersRequest - 116, // 223: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:input_type -> hyapp.wallet.v1.PurchaseResourceShopItemRequest - 119, // 224: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest - 121, // 225: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:input_type -> hyapp.wallet.v1.GetRechargeBillSummaryRequest - 125, // 226: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:input_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsRequest - 127, // 227: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:input_type -> hyapp.wallet.v1.GetRechargeBillOverviewRequest - 133, // 228: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:input_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesRequest - 137, // 229: hyapp.wallet.v1.WalletService.GetWalletOverview:input_type -> hyapp.wallet.v1.GetWalletOverviewRequest - 140, // 230: hyapp.wallet.v1.WalletService.GetWalletValueSummary:input_type -> hyapp.wallet.v1.GetWalletValueSummaryRequest - 143, // 231: hyapp.wallet.v1.WalletService.GetUserGiftWall:input_type -> hyapp.wallet.v1.GetUserGiftWallRequest - 146, // 232: hyapp.wallet.v1.WalletService.ListRechargeProducts:input_type -> hyapp.wallet.v1.ListRechargeProductsRequest - 148, // 233: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:input_type -> hyapp.wallet.v1.ConfirmGooglePaymentRequest - 150, // 234: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:input_type -> hyapp.wallet.v1.ListAdminRechargeProductsRequest - 152, // 235: hyapp.wallet.v1.WalletService.CreateRechargeProduct:input_type -> hyapp.wallet.v1.CreateRechargeProductRequest - 153, // 236: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:input_type -> hyapp.wallet.v1.UpdateRechargeProductRequest - 154, // 237: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:input_type -> hyapp.wallet.v1.DeleteRechargeProductRequest - 159, // 238: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:input_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsRequest - 161, // 239: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:input_type -> hyapp.wallet.v1.SetThirdPartyPaymentMethodStatusRequest - 163, // 240: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:input_type -> hyapp.wallet.v1.UpdateThirdPartyPaymentRateRequest - 165, // 241: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:input_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsRequest - 167, // 242: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:input_type -> hyapp.wallet.v1.H5RechargeOptionsRequest - 170, // 243: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:input_type -> hyapp.wallet.v1.CreateH5RechargeOrderRequest - 171, // 244: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.CreateTemporaryRechargeOrderRequest - 172, // 245: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.GetTemporaryRechargeOrderRequest - 173, // 246: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:input_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersRequest - 175, // 247: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:input_type -> hyapp.wallet.v1.SubmitH5RechargeTxRequest - 176, // 248: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:input_type -> hyapp.wallet.v1.GetH5RechargeOrderRequest - 178, // 249: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:input_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptRequest - 180, // 250: hyapp.wallet.v1.WalletService.HandleMifapayNotify:input_type -> hyapp.wallet.v1.HandleMifapayNotifyRequest - 182, // 251: hyapp.wallet.v1.WalletService.HandleV5PayNotify:input_type -> hyapp.wallet.v1.HandleV5PayNotifyRequest - 185, // 252: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:input_type -> hyapp.wallet.v1.GetDiamondExchangeConfigRequest - 188, // 253: hyapp.wallet.v1.WalletService.ListWalletTransactions:input_type -> hyapp.wallet.v1.ListWalletTransactionsRequest - 198, // 254: hyapp.wallet.v1.WalletService.ListVipPackages:input_type -> hyapp.wallet.v1.ListVipPackagesRequest - 210, // 255: hyapp.wallet.v1.WalletService.GetVipProgramConfig:input_type -> hyapp.wallet.v1.GetVipProgramConfigRequest - 200, // 256: hyapp.wallet.v1.WalletService.GetMyVip:input_type -> hyapp.wallet.v1.GetMyVipRequest - 232, // 257: hyapp.wallet.v1.WalletService.CheckVipBenefit:input_type -> hyapp.wallet.v1.CheckVipBenefitRequest - 202, // 258: hyapp.wallet.v1.WalletService.PurchaseVip:input_type -> hyapp.wallet.v1.PurchaseVipRequest - 204, // 259: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:input_type -> hyapp.wallet.v1.DebitCPBreakupFeeRequest - 206, // 260: hyapp.wallet.v1.WalletService.DebitWheelDraw:input_type -> hyapp.wallet.v1.DebitWheelDrawRequest - 208, // 261: hyapp.wallet.v1.WalletService.GrantVip:input_type -> hyapp.wallet.v1.GrantVipRequest - 226, // 262: hyapp.wallet.v1.WalletService.GrantVipTrialCard:input_type -> hyapp.wallet.v1.GrantVipTrialCardRequest - 228, // 263: hyapp.wallet.v1.WalletService.EquipVipTrialCard:input_type -> hyapp.wallet.v1.EquipVipTrialCardRequest - 230, // 264: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:input_type -> hyapp.wallet.v1.UnequipVipTrialCardRequest - 235, // 265: hyapp.wallet.v1.WalletService.ListAdminVipLevels:input_type -> hyapp.wallet.v1.ListAdminVipLevelsRequest - 237, // 266: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:input_type -> hyapp.wallet.v1.UpdateAdminVipLevelsRequest - 212, // 267: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:input_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest - 214, // 268: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:input_type -> hyapp.wallet.v1.UpdateMyVipSettingsRequest - 220, // 269: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:input_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateRequest - 222, // 270: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:input_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesRequest - 224, // 271: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:input_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateRequest - 239, // 272: hyapp.wallet.v1.WalletService.CreditTaskReward:input_type -> hyapp.wallet.v1.CreditTaskRewardRequest - 241, // 273: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:input_type -> hyapp.wallet.v1.CreditLuckyGiftRewardRequest - 243, // 274: hyapp.wallet.v1.WalletService.CreditWheelReward:input_type -> hyapp.wallet.v1.CreditWheelRewardRequest - 245, // 275: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:input_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardRequest - 247, // 276: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:input_type -> hyapp.wallet.v1.CreditInviteActivityRewardRequest - 249, // 277: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:input_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardRequest - 251, // 278: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:input_type -> hyapp.wallet.v1.ApplyGameCoinChangeRequest - 256, // 279: hyapp.wallet.v1.WalletService.GetRedPacketConfig:input_type -> hyapp.wallet.v1.GetRedPacketConfigRequest - 258, // 280: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:input_type -> hyapp.wallet.v1.UpdateRedPacketConfigRequest - 260, // 281: hyapp.wallet.v1.WalletService.CreateRedPacket:input_type -> hyapp.wallet.v1.CreateRedPacketRequest - 262, // 282: hyapp.wallet.v1.WalletService.ClaimRedPacket:input_type -> hyapp.wallet.v1.ClaimRedPacketRequest - 264, // 283: hyapp.wallet.v1.WalletService.ListRedPackets:input_type -> hyapp.wallet.v1.ListRedPacketsRequest - 266, // 284: hyapp.wallet.v1.WalletService.GetRedPacket:input_type -> hyapp.wallet.v1.GetRedPacketRequest - 268, // 285: hyapp.wallet.v1.WalletService.ExpireRedPackets:input_type -> hyapp.wallet.v1.ExpireRedPacketsRequest - 270, // 286: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:input_type -> hyapp.wallet.v1.RetryRedPacketRefundRequest - 273, // 287: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse - 273, // 288: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse - 273, // 289: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:output_type -> hyapp.wallet.v1.CronBatchResponse - 219, // 290: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:output_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse - 2, // 291: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse - 2, // 292: hyapp.wallet.v1.WalletService.DebitDirectGift:output_type -> hyapp.wallet.v1.DebitGiftResponse - 6, // 293: hyapp.wallet.v1.WalletService.BatchDebitGift:output_type -> hyapp.wallet.v1.BatchDebitGiftResponse - 2, // 294: hyapp.wallet.v1.WalletService.DebitRobotGift:output_type -> hyapp.wallet.v1.DebitGiftResponse - 10, // 295: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse - 14, // 296: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:output_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyResponse - 17, // 297: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:output_type -> hyapp.wallet.v1.GetHostSalaryProgressResponse - 281, // 298: hyapp.wallet.v1.WalletService.GetHostRevenueStats:output_type -> hyapp.wallet.v1.GetHostRevenueStatsResponse - 284, // 299: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:output_type -> hyapp.wallet.v1.GetAgencyPointShareStatsResponse - 287, // 300: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:output_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsResponse - 20, // 301: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:output_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsResponse - 22, // 302: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse - 24, // 303: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse - 26, // 304: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse - 28, // 305: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:output_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildResponse - 31, // 306: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:output_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersResponse - 33, // 307: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:output_type -> hyapp.wallet.v1.ExchangeSalaryToCoinResponse - 35, // 308: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:output_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerResponse - 290, // 309: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:output_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse - 292, // 310: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:output_type -> hyapp.wallet.v1.TransferPointToCoinSellerResponse - 294, // 311: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:output_type -> hyapp.wallet.v1.GetPointWithdrawalConfigResponse - 37, // 312: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:output_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalResponse - 39, // 313: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:output_type -> hyapp.wallet.v1.SettleSalaryWithdrawalResponse - 41, // 314: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:output_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalResponse - 43, // 315: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:output_type -> hyapp.wallet.v1.FreezePointWithdrawalResponse - 45, // 316: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:output_type -> hyapp.wallet.v1.SettlePointWithdrawalResponse - 47, // 317: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:output_type -> hyapp.wallet.v1.ReleasePointWithdrawalResponse - 62, // 318: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse - 64, // 319: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse - 71, // 320: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse - 71, // 321: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse - 71, // 322: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse - 71, // 323: hyapp.wallet.v1.WalletService.DeleteResource:output_type -> hyapp.wallet.v1.ResourceResponse - 70, // 324: hyapp.wallet.v1.WalletService.BatchDeleteResources:output_type -> hyapp.wallet.v1.BatchDeleteResourcesResponse - 73, // 325: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse - 75, // 326: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse - 79, // 327: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse - 79, // 328: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse - 79, // 329: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse - 53, // 330: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:output_type -> hyapp.wallet.v1.PinResourceGroupSnapshotResponse - 81, // 331: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse - 83, // 332: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:output_type -> hyapp.wallet.v1.ListGiftTypeConfigsResponse - 278, // 333: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:output_type -> hyapp.wallet.v1.GetGiftCatalogVersionResponse - 92, // 334: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse - 88, // 335: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:output_type -> hyapp.wallet.v1.BatchCreateGiftConfigsResponse - 92, // 336: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse - 92, // 337: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse - 92, // 338: hyapp.wallet.v1.WalletService.DeleteGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse - 85, // 339: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:output_type -> hyapp.wallet.v1.GiftTypeConfigResponse - 97, // 340: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 97, // 341: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 97, // 342: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 97, // 343: hyapp.wallet.v1.WalletService.RevokeResourceGrant:output_type -> hyapp.wallet.v1.ResourceGrantResponse - 299, // 344: hyapp.wallet.v1.WalletService.RevokeUserResource:output_type -> hyapp.wallet.v1.RevokeUserResourceResponse - 99, // 345: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse - 101, // 346: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse - 103, // 347: hyapp.wallet.v1.WalletService.UnequipUserResource:output_type -> hyapp.wallet.v1.UnequipUserResourceResponse - 106, // 348: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:output_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesResponse - 108, // 349: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse - 111, // 350: hyapp.wallet.v1.WalletService.ListResourceShopItems:output_type -> hyapp.wallet.v1.ListResourceShopItemsResponse - 113, // 351: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:output_type -> hyapp.wallet.v1.UpsertResourceShopItemsResponse - 115, // 352: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:output_type -> hyapp.wallet.v1.ResourceShopItemResponse - 276, // 353: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:output_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse - 117, // 354: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:output_type -> hyapp.wallet.v1.PurchaseResourceShopItemResponse - 120, // 355: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse - 123, // 356: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:output_type -> hyapp.wallet.v1.GetRechargeBillSummaryResponse - 126, // 357: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:output_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsResponse - 132, // 358: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:output_type -> hyapp.wallet.v1.GetRechargeBillOverviewResponse - 135, // 359: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:output_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesResponse - 138, // 360: hyapp.wallet.v1.WalletService.GetWalletOverview:output_type -> hyapp.wallet.v1.GetWalletOverviewResponse - 141, // 361: hyapp.wallet.v1.WalletService.GetWalletValueSummary:output_type -> hyapp.wallet.v1.GetWalletValueSummaryResponse - 144, // 362: hyapp.wallet.v1.WalletService.GetUserGiftWall:output_type -> hyapp.wallet.v1.GetUserGiftWallResponse - 147, // 363: hyapp.wallet.v1.WalletService.ListRechargeProducts:output_type -> hyapp.wallet.v1.ListRechargeProductsResponse - 149, // 364: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:output_type -> hyapp.wallet.v1.ConfirmGooglePaymentResponse - 151, // 365: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:output_type -> hyapp.wallet.v1.ListAdminRechargeProductsResponse - 155, // 366: hyapp.wallet.v1.WalletService.CreateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse - 155, // 367: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse - 156, // 368: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:output_type -> hyapp.wallet.v1.DeleteRechargeProductResponse - 160, // 369: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:output_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsResponse - 162, // 370: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse - 162, // 371: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse - 166, // 372: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:output_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsResponse - 168, // 373: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:output_type -> hyapp.wallet.v1.H5RechargeOptionsResponse - 177, // 374: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 177, // 375: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 177, // 376: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 174, // 377: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:output_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersResponse - 177, // 378: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 177, // 379: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse - 179, // 380: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:output_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptResponse - 181, // 381: hyapp.wallet.v1.WalletService.HandleMifapayNotify:output_type -> hyapp.wallet.v1.HandleMifapayNotifyResponse - 183, // 382: hyapp.wallet.v1.WalletService.HandleV5PayNotify:output_type -> hyapp.wallet.v1.HandleV5PayNotifyResponse - 186, // 383: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:output_type -> hyapp.wallet.v1.GetDiamondExchangeConfigResponse - 189, // 384: hyapp.wallet.v1.WalletService.ListWalletTransactions:output_type -> hyapp.wallet.v1.ListWalletTransactionsResponse - 199, // 385: hyapp.wallet.v1.WalletService.ListVipPackages:output_type -> hyapp.wallet.v1.ListVipPackagesResponse - 211, // 386: hyapp.wallet.v1.WalletService.GetVipProgramConfig:output_type -> hyapp.wallet.v1.GetVipProgramConfigResponse - 201, // 387: hyapp.wallet.v1.WalletService.GetMyVip:output_type -> hyapp.wallet.v1.GetMyVipResponse - 233, // 388: hyapp.wallet.v1.WalletService.CheckVipBenefit:output_type -> hyapp.wallet.v1.CheckVipBenefitResponse - 203, // 389: hyapp.wallet.v1.WalletService.PurchaseVip:output_type -> hyapp.wallet.v1.PurchaseVipResponse - 205, // 390: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:output_type -> hyapp.wallet.v1.DebitCPBreakupFeeResponse - 207, // 391: hyapp.wallet.v1.WalletService.DebitWheelDraw:output_type -> hyapp.wallet.v1.DebitWheelDrawResponse - 209, // 392: hyapp.wallet.v1.WalletService.GrantVip:output_type -> hyapp.wallet.v1.GrantVipResponse - 227, // 393: hyapp.wallet.v1.WalletService.GrantVipTrialCard:output_type -> hyapp.wallet.v1.GrantVipTrialCardResponse - 229, // 394: hyapp.wallet.v1.WalletService.EquipVipTrialCard:output_type -> hyapp.wallet.v1.EquipVipTrialCardResponse - 231, // 395: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:output_type -> hyapp.wallet.v1.UnequipVipTrialCardResponse - 236, // 396: hyapp.wallet.v1.WalletService.ListAdminVipLevels:output_type -> hyapp.wallet.v1.ListAdminVipLevelsResponse - 238, // 397: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:output_type -> hyapp.wallet.v1.UpdateAdminVipLevelsResponse - 213, // 398: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:output_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse - 215, // 399: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:output_type -> hyapp.wallet.v1.UpdateMyVipSettingsResponse - 221, // 400: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:output_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse - 223, // 401: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:output_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse - 225, // 402: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:output_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse - 240, // 403: hyapp.wallet.v1.WalletService.CreditTaskReward:output_type -> hyapp.wallet.v1.CreditTaskRewardResponse - 242, // 404: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:output_type -> hyapp.wallet.v1.CreditLuckyGiftRewardResponse - 244, // 405: hyapp.wallet.v1.WalletService.CreditWheelReward:output_type -> hyapp.wallet.v1.CreditWheelRewardResponse - 246, // 406: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:output_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardResponse - 248, // 407: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:output_type -> hyapp.wallet.v1.CreditInviteActivityRewardResponse - 250, // 408: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:output_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardResponse - 252, // 409: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:output_type -> hyapp.wallet.v1.ApplyGameCoinChangeResponse - 257, // 410: hyapp.wallet.v1.WalletService.GetRedPacketConfig:output_type -> hyapp.wallet.v1.GetRedPacketConfigResponse - 259, // 411: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:output_type -> hyapp.wallet.v1.UpdateRedPacketConfigResponse - 261, // 412: hyapp.wallet.v1.WalletService.CreateRedPacket:output_type -> hyapp.wallet.v1.CreateRedPacketResponse - 263, // 413: hyapp.wallet.v1.WalletService.ClaimRedPacket:output_type -> hyapp.wallet.v1.ClaimRedPacketResponse - 265, // 414: hyapp.wallet.v1.WalletService.ListRedPackets:output_type -> hyapp.wallet.v1.ListRedPacketsResponse - 267, // 415: hyapp.wallet.v1.WalletService.GetRedPacket:output_type -> hyapp.wallet.v1.GetRedPacketResponse - 269, // 416: hyapp.wallet.v1.WalletService.ExpireRedPackets:output_type -> hyapp.wallet.v1.ExpireRedPacketsResponse - 271, // 417: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:output_type -> hyapp.wallet.v1.RetryRedPacketRefundResponse - 287, // [287:418] is the sub-list for method output_type - 156, // [156:287] is the sub-list for method input_type - 156, // [156:156] is the sub-list for extension type_name - 156, // [156:156] is the sub-list for extension extendee - 0, // [0:156] is the sub-list for field type_name + 48, // 86: hyapp.wallet.v1.VipBenefit.resource:type_name -> hyapp.wallet.v1.Resource + 190, // 87: hyapp.wallet.v1.VipLevel.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem + 192, // 88: hyapp.wallet.v1.VipLevel.benefits:type_name -> hyapp.wallet.v1.VipBenefit + 194, // 89: hyapp.wallet.v1.VipState.paid_vip:type_name -> hyapp.wallet.v1.UserVip + 195, // 90: hyapp.wallet.v1.VipState.equipped_trial_card:type_name -> hyapp.wallet.v1.VipTrialCard + 194, // 91: hyapp.wallet.v1.VipState.effective_vip:type_name -> hyapp.wallet.v1.UserVip + 192, // 92: hyapp.wallet.v1.VipState.effective_benefits:type_name -> hyapp.wallet.v1.VipBenefit + 191, // 93: hyapp.wallet.v1.VipState.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig + 196, // 94: hyapp.wallet.v1.VipState.user_settings:type_name -> hyapp.wallet.v1.VipUserSettings + 194, // 95: hyapp.wallet.v1.ListVipPackagesResponse.current_vip:type_name -> hyapp.wallet.v1.UserVip + 193, // 96: hyapp.wallet.v1.ListVipPackagesResponse.packages:type_name -> hyapp.wallet.v1.VipLevel + 197, // 97: hyapp.wallet.v1.ListVipPackagesResponse.state:type_name -> hyapp.wallet.v1.VipState + 191, // 98: hyapp.wallet.v1.ListVipPackagesResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig + 194, // 99: hyapp.wallet.v1.GetMyVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip + 197, // 100: hyapp.wallet.v1.GetMyVipResponse.state:type_name -> hyapp.wallet.v1.VipState + 194, // 101: hyapp.wallet.v1.PurchaseVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip + 190, // 102: hyapp.wallet.v1.PurchaseVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem + 197, // 103: hyapp.wallet.v1.PurchaseVipResponse.state:type_name -> hyapp.wallet.v1.VipState + 8, // 104: hyapp.wallet.v1.PurchaseVipResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 105: hyapp.wallet.v1.DebitCPBreakupFeeResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 106: hyapp.wallet.v1.DebitWheelDrawResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 194, // 107: hyapp.wallet.v1.GrantVipResponse.vip:type_name -> hyapp.wallet.v1.UserVip + 190, // 108: hyapp.wallet.v1.GrantVipResponse.reward_items:type_name -> hyapp.wallet.v1.VipRewardItem + 197, // 109: hyapp.wallet.v1.GrantVipResponse.state:type_name -> hyapp.wallet.v1.VipState + 191, // 110: hyapp.wallet.v1.GetVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig + 192, // 111: hyapp.wallet.v1.GetVipProgramConfigResponse.benefits:type_name -> hyapp.wallet.v1.VipBenefit + 191, // 112: hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest.config:type_name -> hyapp.wallet.v1.VipProgramConfig + 191, // 113: hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse.config:type_name -> hyapp.wallet.v1.VipProgramConfig + 196, // 114: hyapp.wallet.v1.UpdateMyVipSettingsResponse.settings:type_name -> hyapp.wallet.v1.VipUserSettings + 216, // 115: hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse.run:type_name -> hyapp.wallet.v1.VipDailyCoinRebateRun + 217, // 116: hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate + 217, // 117: hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse.rebates:type_name -> hyapp.wallet.v1.VipDailyCoinRebate + 217, // 118: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.rebate:type_name -> hyapp.wallet.v1.VipDailyCoinRebate + 8, // 119: hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse.coin_balance:type_name -> hyapp.wallet.v1.AssetBalance + 195, // 120: hyapp.wallet.v1.GrantVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard + 197, // 121: hyapp.wallet.v1.GrantVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState + 195, // 122: hyapp.wallet.v1.EquipVipTrialCardResponse.trial_card:type_name -> hyapp.wallet.v1.VipTrialCard + 197, // 123: hyapp.wallet.v1.EquipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState + 197, // 124: hyapp.wallet.v1.UnequipVipTrialCardResponse.state:type_name -> hyapp.wallet.v1.VipState + 192, // 125: hyapp.wallet.v1.CheckVipBenefitResponse.benefit:type_name -> hyapp.wallet.v1.VipBenefit + 197, // 126: hyapp.wallet.v1.CheckVipBenefitResponse.state:type_name -> hyapp.wallet.v1.VipState + 192, // 127: hyapp.wallet.v1.AdminVipLevelInput.benefits:type_name -> hyapp.wallet.v1.VipBenefit + 193, // 128: hyapp.wallet.v1.ListAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel + 191, // 129: hyapp.wallet.v1.ListAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig + 234, // 130: hyapp.wallet.v1.UpdateAdminVipLevelsRequest.levels:type_name -> hyapp.wallet.v1.AdminVipLevelInput + 193, // 131: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.levels:type_name -> hyapp.wallet.v1.VipLevel + 191, // 132: hyapp.wallet.v1.UpdateAdminVipLevelsResponse.program_config:type_name -> hyapp.wallet.v1.VipProgramConfig + 8, // 133: hyapp.wallet.v1.CreditTaskRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 134: hyapp.wallet.v1.CreditLuckyGiftRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 135: hyapp.wallet.v1.CreditWheelRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 136: hyapp.wallet.v1.CreditRoomTurnoverRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 137: hyapp.wallet.v1.CreditInviteActivityRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 8, // 138: hyapp.wallet.v1.CreditAgencyOpeningRewardResponse.balance:type_name -> hyapp.wallet.v1.AssetBalance + 254, // 139: hyapp.wallet.v1.RedPacket.claims:type_name -> hyapp.wallet.v1.RedPacketClaim + 253, // 140: hyapp.wallet.v1.GetRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig + 253, // 141: hyapp.wallet.v1.UpdateRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig + 255, // 142: hyapp.wallet.v1.CreateRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket + 254, // 143: hyapp.wallet.v1.ClaimRedPacketResponse.claim:type_name -> hyapp.wallet.v1.RedPacketClaim + 255, // 144: hyapp.wallet.v1.ClaimRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket + 255, // 145: hyapp.wallet.v1.ListRedPacketsResponse.packets:type_name -> hyapp.wallet.v1.RedPacket + 255, // 146: hyapp.wallet.v1.GetRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket + 255, // 147: hyapp.wallet.v1.RetryRedPacketRefundResponse.packet:type_name -> hyapp.wallet.v1.RedPacket + 48, // 148: hyapp.wallet.v1.ResourceShopPurchaseOrder.resource:type_name -> hyapp.wallet.v1.Resource + 274, // 149: hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse.orders:type_name -> hyapp.wallet.v1.ResourceShopPurchaseOrder + 279, // 150: hyapp.wallet.v1.GetHostRevenueStatsResponse.stats:type_name -> hyapp.wallet.v1.HostRevenueStats + 282, // 151: hyapp.wallet.v1.GetAgencyPointShareStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyPointShareStats + 285, // 152: hyapp.wallet.v1.GetAgencyHostGiftStatsResponse.stats:type_name -> hyapp.wallet.v1.AgencyHostGiftStats + 288, // 153: hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse.sellers:type_name -> hyapp.wallet.v1.PointWithdrawalCoinSellerConfig + 295, // 154: hyapp.wallet.v1.VipBenefitPresentation.preview_items:type_name -> hyapp.wallet.v1.VipBenefitPreviewItem + 296, // 155: hyapp.wallet.v1.VipBenefitPresentation.numeric_reward:type_name -> hyapp.wallet.v1.VipBenefitNumericReward + 57, // 156: hyapp.wallet.v1.RevokeUserResourceResponse.resource:type_name -> hyapp.wallet.v1.UserResourceEntitlement + 272, // 157: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest + 272, // 158: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:input_type -> hyapp.wallet.v1.CronBatchRequest + 272, // 159: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:input_type -> hyapp.wallet.v1.CronBatchRequest + 218, // 160: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:input_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchRequest + 0, // 161: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest + 1, // 162: hyapp.wallet.v1.WalletService.DebitDirectGift:input_type -> hyapp.wallet.v1.DebitDirectGiftRequest + 4, // 163: hyapp.wallet.v1.WalletService.BatchDebitGift:input_type -> hyapp.wallet.v1.BatchDebitGiftRequest + 7, // 164: hyapp.wallet.v1.WalletService.DebitRobotGift:input_type -> hyapp.wallet.v1.DebitRobotGiftRequest + 9, // 165: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest + 13, // 166: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:input_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyRequest + 16, // 167: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:input_type -> hyapp.wallet.v1.GetHostSalaryProgressRequest + 280, // 168: hyapp.wallet.v1.WalletService.GetHostRevenueStats:input_type -> hyapp.wallet.v1.GetHostRevenueStatsRequest + 283, // 169: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:input_type -> hyapp.wallet.v1.GetAgencyPointShareStatsRequest + 286, // 170: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:input_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsRequest + 19, // 171: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:input_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsRequest + 21, // 172: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest + 23, // 173: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest + 25, // 174: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest + 27, // 175: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:input_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildRequest + 30, // 176: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:input_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersRequest + 32, // 177: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:input_type -> hyapp.wallet.v1.ExchangeSalaryToCoinRequest + 34, // 178: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:input_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerRequest + 289, // 179: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:input_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersRequest + 291, // 180: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:input_type -> hyapp.wallet.v1.TransferPointToCoinSellerRequest + 293, // 181: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:input_type -> hyapp.wallet.v1.GetPointWithdrawalConfigRequest + 36, // 182: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:input_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalRequest + 38, // 183: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:input_type -> hyapp.wallet.v1.SettleSalaryWithdrawalRequest + 40, // 184: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:input_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalRequest + 42, // 185: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:input_type -> hyapp.wallet.v1.FreezePointWithdrawalRequest + 44, // 186: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:input_type -> hyapp.wallet.v1.SettlePointWithdrawalRequest + 46, // 187: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:input_type -> hyapp.wallet.v1.ReleasePointWithdrawalRequest + 61, // 188: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest + 63, // 189: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest + 65, // 190: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest + 66, // 191: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest + 67, // 192: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest + 68, // 193: hyapp.wallet.v1.WalletService.DeleteResource:input_type -> hyapp.wallet.v1.DeleteResourceRequest + 69, // 194: hyapp.wallet.v1.WalletService.BatchDeleteResources:input_type -> hyapp.wallet.v1.BatchDeleteResourcesRequest + 72, // 195: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest + 74, // 196: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest + 76, // 197: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest + 77, // 198: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest + 78, // 199: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest + 52, // 200: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:input_type -> hyapp.wallet.v1.PinResourceGroupSnapshotRequest + 80, // 201: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest + 82, // 202: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:input_type -> hyapp.wallet.v1.ListGiftTypeConfigsRequest + 277, // 203: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:input_type -> hyapp.wallet.v1.GetGiftCatalogVersionRequest + 86, // 204: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest + 87, // 205: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:input_type -> hyapp.wallet.v1.BatchCreateGiftConfigsRequest + 89, // 206: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest + 90, // 207: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest + 91, // 208: hyapp.wallet.v1.WalletService.DeleteGiftConfig:input_type -> hyapp.wallet.v1.DeleteGiftConfigRequest + 84, // 209: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:input_type -> hyapp.wallet.v1.UpsertGiftTypeConfigRequest + 93, // 210: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest + 94, // 211: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest + 95, // 212: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:input_type -> hyapp.wallet.v1.GrantPinnedResourceGroupRequest + 96, // 213: hyapp.wallet.v1.WalletService.RevokeResourceGrant:input_type -> hyapp.wallet.v1.RevokeResourceGrantRequest + 298, // 214: hyapp.wallet.v1.WalletService.RevokeUserResource:input_type -> hyapp.wallet.v1.RevokeUserResourceRequest + 98, // 215: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest + 100, // 216: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest + 102, // 217: hyapp.wallet.v1.WalletService.UnequipUserResource:input_type -> hyapp.wallet.v1.UnequipUserResourceRequest + 104, // 218: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:input_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesRequest + 107, // 219: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest + 110, // 220: hyapp.wallet.v1.WalletService.ListResourceShopItems:input_type -> hyapp.wallet.v1.ListResourceShopItemsRequest + 112, // 221: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:input_type -> hyapp.wallet.v1.UpsertResourceShopItemsRequest + 114, // 222: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:input_type -> hyapp.wallet.v1.SetResourceShopItemStatusRequest + 275, // 223: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:input_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersRequest + 116, // 224: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:input_type -> hyapp.wallet.v1.PurchaseResourceShopItemRequest + 119, // 225: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest + 121, // 226: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:input_type -> hyapp.wallet.v1.GetRechargeBillSummaryRequest + 125, // 227: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:input_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsRequest + 127, // 228: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:input_type -> hyapp.wallet.v1.GetRechargeBillOverviewRequest + 133, // 229: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:input_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesRequest + 137, // 230: hyapp.wallet.v1.WalletService.GetWalletOverview:input_type -> hyapp.wallet.v1.GetWalletOverviewRequest + 140, // 231: hyapp.wallet.v1.WalletService.GetWalletValueSummary:input_type -> hyapp.wallet.v1.GetWalletValueSummaryRequest + 143, // 232: hyapp.wallet.v1.WalletService.GetUserGiftWall:input_type -> hyapp.wallet.v1.GetUserGiftWallRequest + 146, // 233: hyapp.wallet.v1.WalletService.ListRechargeProducts:input_type -> hyapp.wallet.v1.ListRechargeProductsRequest + 148, // 234: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:input_type -> hyapp.wallet.v1.ConfirmGooglePaymentRequest + 150, // 235: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:input_type -> hyapp.wallet.v1.ListAdminRechargeProductsRequest + 152, // 236: hyapp.wallet.v1.WalletService.CreateRechargeProduct:input_type -> hyapp.wallet.v1.CreateRechargeProductRequest + 153, // 237: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:input_type -> hyapp.wallet.v1.UpdateRechargeProductRequest + 154, // 238: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:input_type -> hyapp.wallet.v1.DeleteRechargeProductRequest + 159, // 239: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:input_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsRequest + 161, // 240: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:input_type -> hyapp.wallet.v1.SetThirdPartyPaymentMethodStatusRequest + 163, // 241: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:input_type -> hyapp.wallet.v1.UpdateThirdPartyPaymentRateRequest + 165, // 242: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:input_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsRequest + 167, // 243: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:input_type -> hyapp.wallet.v1.H5RechargeOptionsRequest + 170, // 244: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:input_type -> hyapp.wallet.v1.CreateH5RechargeOrderRequest + 171, // 245: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.CreateTemporaryRechargeOrderRequest + 172, // 246: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:input_type -> hyapp.wallet.v1.GetTemporaryRechargeOrderRequest + 173, // 247: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:input_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersRequest + 175, // 248: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:input_type -> hyapp.wallet.v1.SubmitH5RechargeTxRequest + 176, // 249: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:input_type -> hyapp.wallet.v1.GetH5RechargeOrderRequest + 178, // 250: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:input_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptRequest + 180, // 251: hyapp.wallet.v1.WalletService.HandleMifapayNotify:input_type -> hyapp.wallet.v1.HandleMifapayNotifyRequest + 182, // 252: hyapp.wallet.v1.WalletService.HandleV5PayNotify:input_type -> hyapp.wallet.v1.HandleV5PayNotifyRequest + 185, // 253: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:input_type -> hyapp.wallet.v1.GetDiamondExchangeConfigRequest + 188, // 254: hyapp.wallet.v1.WalletService.ListWalletTransactions:input_type -> hyapp.wallet.v1.ListWalletTransactionsRequest + 198, // 255: hyapp.wallet.v1.WalletService.ListVipPackages:input_type -> hyapp.wallet.v1.ListVipPackagesRequest + 210, // 256: hyapp.wallet.v1.WalletService.GetVipProgramConfig:input_type -> hyapp.wallet.v1.GetVipProgramConfigRequest + 200, // 257: hyapp.wallet.v1.WalletService.GetMyVip:input_type -> hyapp.wallet.v1.GetMyVipRequest + 232, // 258: hyapp.wallet.v1.WalletService.CheckVipBenefit:input_type -> hyapp.wallet.v1.CheckVipBenefitRequest + 202, // 259: hyapp.wallet.v1.WalletService.PurchaseVip:input_type -> hyapp.wallet.v1.PurchaseVipRequest + 204, // 260: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:input_type -> hyapp.wallet.v1.DebitCPBreakupFeeRequest + 206, // 261: hyapp.wallet.v1.WalletService.DebitWheelDraw:input_type -> hyapp.wallet.v1.DebitWheelDrawRequest + 208, // 262: hyapp.wallet.v1.WalletService.GrantVip:input_type -> hyapp.wallet.v1.GrantVipRequest + 226, // 263: hyapp.wallet.v1.WalletService.GrantVipTrialCard:input_type -> hyapp.wallet.v1.GrantVipTrialCardRequest + 228, // 264: hyapp.wallet.v1.WalletService.EquipVipTrialCard:input_type -> hyapp.wallet.v1.EquipVipTrialCardRequest + 230, // 265: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:input_type -> hyapp.wallet.v1.UnequipVipTrialCardRequest + 235, // 266: hyapp.wallet.v1.WalletService.ListAdminVipLevels:input_type -> hyapp.wallet.v1.ListAdminVipLevelsRequest + 237, // 267: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:input_type -> hyapp.wallet.v1.UpdateAdminVipLevelsRequest + 212, // 268: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:input_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigRequest + 214, // 269: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:input_type -> hyapp.wallet.v1.UpdateMyVipSettingsRequest + 220, // 270: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:input_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateRequest + 222, // 271: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:input_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesRequest + 224, // 272: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:input_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateRequest + 239, // 273: hyapp.wallet.v1.WalletService.CreditTaskReward:input_type -> hyapp.wallet.v1.CreditTaskRewardRequest + 241, // 274: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:input_type -> hyapp.wallet.v1.CreditLuckyGiftRewardRequest + 243, // 275: hyapp.wallet.v1.WalletService.CreditWheelReward:input_type -> hyapp.wallet.v1.CreditWheelRewardRequest + 245, // 276: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:input_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardRequest + 247, // 277: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:input_type -> hyapp.wallet.v1.CreditInviteActivityRewardRequest + 249, // 278: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:input_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardRequest + 251, // 279: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:input_type -> hyapp.wallet.v1.ApplyGameCoinChangeRequest + 256, // 280: hyapp.wallet.v1.WalletService.GetRedPacketConfig:input_type -> hyapp.wallet.v1.GetRedPacketConfigRequest + 258, // 281: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:input_type -> hyapp.wallet.v1.UpdateRedPacketConfigRequest + 260, // 282: hyapp.wallet.v1.WalletService.CreateRedPacket:input_type -> hyapp.wallet.v1.CreateRedPacketRequest + 262, // 283: hyapp.wallet.v1.WalletService.ClaimRedPacket:input_type -> hyapp.wallet.v1.ClaimRedPacketRequest + 264, // 284: hyapp.wallet.v1.WalletService.ListRedPackets:input_type -> hyapp.wallet.v1.ListRedPacketsRequest + 266, // 285: hyapp.wallet.v1.WalletService.GetRedPacket:input_type -> hyapp.wallet.v1.GetRedPacketRequest + 268, // 286: hyapp.wallet.v1.WalletService.ExpireRedPackets:input_type -> hyapp.wallet.v1.ExpireRedPacketsRequest + 270, // 287: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:input_type -> hyapp.wallet.v1.RetryRedPacketRefundRequest + 273, // 288: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryDailySettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse + 273, // 289: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryHalfMonthSettlementBatch:output_type -> hyapp.wallet.v1.CronBatchResponse + 273, // 290: hyapp.wallet.v1.WalletCronService.ProcessHostSalaryMonthEndBatch:output_type -> hyapp.wallet.v1.CronBatchResponse + 219, // 291: hyapp.wallet.v1.WalletCronService.ProcessVipDailyCoinRebateBatch:output_type -> hyapp.wallet.v1.ProcessVipDailyCoinRebateBatchResponse + 2, // 292: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse + 2, // 293: hyapp.wallet.v1.WalletService.DebitDirectGift:output_type -> hyapp.wallet.v1.DebitGiftResponse + 6, // 294: hyapp.wallet.v1.WalletService.BatchDebitGift:output_type -> hyapp.wallet.v1.BatchDebitGiftResponse + 2, // 295: hyapp.wallet.v1.WalletService.DebitRobotGift:output_type -> hyapp.wallet.v1.DebitGiftResponse + 10, // 296: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse + 14, // 297: hyapp.wallet.v1.WalletService.GetActiveHostSalaryPolicy:output_type -> hyapp.wallet.v1.GetActiveHostSalaryPolicyResponse + 17, // 298: hyapp.wallet.v1.WalletService.GetHostSalaryProgress:output_type -> hyapp.wallet.v1.GetHostSalaryProgressResponse + 281, // 299: hyapp.wallet.v1.WalletService.GetHostRevenueStats:output_type -> hyapp.wallet.v1.GetHostRevenueStatsResponse + 284, // 300: hyapp.wallet.v1.WalletService.GetAgencyPointShareStats:output_type -> hyapp.wallet.v1.GetAgencyPointShareStatsResponse + 287, // 301: hyapp.wallet.v1.WalletService.GetAgencyHostGiftStats:output_type -> hyapp.wallet.v1.GetAgencyHostGiftStatsResponse + 20, // 302: hyapp.wallet.v1.WalletService.GetTeamHostSalaryStats:output_type -> hyapp.wallet.v1.GetTeamHostSalaryStatsResponse + 22, // 303: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse + 24, // 304: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse + 26, // 305: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse + 28, // 306: hyapp.wallet.v1.WalletService.TransferCoinSellerStockToChild:output_type -> hyapp.wallet.v1.TransferCoinSellerStockToChildResponse + 31, // 307: hyapp.wallet.v1.WalletService.ListCoinSellerSalaryExchangeRateTiers:output_type -> hyapp.wallet.v1.ListCoinSellerSalaryExchangeRateTiersResponse + 33, // 308: hyapp.wallet.v1.WalletService.ExchangeSalaryToCoin:output_type -> hyapp.wallet.v1.ExchangeSalaryToCoinResponse + 35, // 309: hyapp.wallet.v1.WalletService.TransferSalaryToCoinSeller:output_type -> hyapp.wallet.v1.TransferSalaryToCoinSellerResponse + 290, // 310: hyapp.wallet.v1.WalletService.ListPointWithdrawalCoinSellers:output_type -> hyapp.wallet.v1.ListPointWithdrawalCoinSellersResponse + 292, // 311: hyapp.wallet.v1.WalletService.TransferPointToCoinSeller:output_type -> hyapp.wallet.v1.TransferPointToCoinSellerResponse + 294, // 312: hyapp.wallet.v1.WalletService.GetPointWithdrawalConfig:output_type -> hyapp.wallet.v1.GetPointWithdrawalConfigResponse + 37, // 313: hyapp.wallet.v1.WalletService.FreezeSalaryWithdrawal:output_type -> hyapp.wallet.v1.FreezeSalaryWithdrawalResponse + 39, // 314: hyapp.wallet.v1.WalletService.SettleSalaryWithdrawal:output_type -> hyapp.wallet.v1.SettleSalaryWithdrawalResponse + 41, // 315: hyapp.wallet.v1.WalletService.ReleaseSalaryWithdrawal:output_type -> hyapp.wallet.v1.ReleaseSalaryWithdrawalResponse + 43, // 316: hyapp.wallet.v1.WalletService.FreezePointWithdrawal:output_type -> hyapp.wallet.v1.FreezePointWithdrawalResponse + 45, // 317: hyapp.wallet.v1.WalletService.SettlePointWithdrawal:output_type -> hyapp.wallet.v1.SettlePointWithdrawalResponse + 47, // 318: hyapp.wallet.v1.WalletService.ReleasePointWithdrawal:output_type -> hyapp.wallet.v1.ReleasePointWithdrawalResponse + 62, // 319: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse + 64, // 320: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse + 71, // 321: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse + 71, // 322: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse + 71, // 323: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse + 71, // 324: hyapp.wallet.v1.WalletService.DeleteResource:output_type -> hyapp.wallet.v1.ResourceResponse + 70, // 325: hyapp.wallet.v1.WalletService.BatchDeleteResources:output_type -> hyapp.wallet.v1.BatchDeleteResourcesResponse + 73, // 326: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse + 75, // 327: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse + 79, // 328: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 79, // 329: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 79, // 330: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse + 53, // 331: hyapp.wallet.v1.WalletService.PinResourceGroupSnapshot:output_type -> hyapp.wallet.v1.PinResourceGroupSnapshotResponse + 81, // 332: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse + 83, // 333: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:output_type -> hyapp.wallet.v1.ListGiftTypeConfigsResponse + 278, // 334: hyapp.wallet.v1.WalletService.GetGiftCatalogVersion:output_type -> hyapp.wallet.v1.GetGiftCatalogVersionResponse + 92, // 335: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 88, // 336: hyapp.wallet.v1.WalletService.BatchCreateGiftConfigs:output_type -> hyapp.wallet.v1.BatchCreateGiftConfigsResponse + 92, // 337: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 92, // 338: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse + 92, // 339: hyapp.wallet.v1.WalletService.DeleteGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse + 85, // 340: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:output_type -> hyapp.wallet.v1.GiftTypeConfigResponse + 97, // 341: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 97, // 342: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 97, // 343: hyapp.wallet.v1.WalletService.GrantPinnedResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 97, // 344: hyapp.wallet.v1.WalletService.RevokeResourceGrant:output_type -> hyapp.wallet.v1.ResourceGrantResponse + 299, // 345: hyapp.wallet.v1.WalletService.RevokeUserResource:output_type -> hyapp.wallet.v1.RevokeUserResourceResponse + 99, // 346: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse + 101, // 347: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse + 103, // 348: hyapp.wallet.v1.WalletService.UnequipUserResource:output_type -> hyapp.wallet.v1.UnequipUserResourceResponse + 106, // 349: hyapp.wallet.v1.WalletService.BatchGetUserEquippedResources:output_type -> hyapp.wallet.v1.BatchGetUserEquippedResourcesResponse + 108, // 350: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse + 111, // 351: hyapp.wallet.v1.WalletService.ListResourceShopItems:output_type -> hyapp.wallet.v1.ListResourceShopItemsResponse + 113, // 352: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:output_type -> hyapp.wallet.v1.UpsertResourceShopItemsResponse + 115, // 353: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:output_type -> hyapp.wallet.v1.ResourceShopItemResponse + 276, // 354: hyapp.wallet.v1.WalletService.ListResourceShopPurchaseOrders:output_type -> hyapp.wallet.v1.ListResourceShopPurchaseOrdersResponse + 117, // 355: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:output_type -> hyapp.wallet.v1.PurchaseResourceShopItemResponse + 120, // 356: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse + 123, // 357: hyapp.wallet.v1.WalletService.GetRechargeBillSummary:output_type -> hyapp.wallet.v1.GetRechargeBillSummaryResponse + 126, // 358: hyapp.wallet.v1.WalletService.BatchGetUserRechargeStats:output_type -> hyapp.wallet.v1.BatchGetUserRechargeStatsResponse + 132, // 359: hyapp.wallet.v1.WalletService.GetRechargeBillOverview:output_type -> hyapp.wallet.v1.GetRechargeBillOverviewResponse + 135, // 360: hyapp.wallet.v1.WalletService.RefreshGooglePaymentPrices:output_type -> hyapp.wallet.v1.RefreshGooglePaymentPricesResponse + 138, // 361: hyapp.wallet.v1.WalletService.GetWalletOverview:output_type -> hyapp.wallet.v1.GetWalletOverviewResponse + 141, // 362: hyapp.wallet.v1.WalletService.GetWalletValueSummary:output_type -> hyapp.wallet.v1.GetWalletValueSummaryResponse + 144, // 363: hyapp.wallet.v1.WalletService.GetUserGiftWall:output_type -> hyapp.wallet.v1.GetUserGiftWallResponse + 147, // 364: hyapp.wallet.v1.WalletService.ListRechargeProducts:output_type -> hyapp.wallet.v1.ListRechargeProductsResponse + 149, // 365: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:output_type -> hyapp.wallet.v1.ConfirmGooglePaymentResponse + 151, // 366: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:output_type -> hyapp.wallet.v1.ListAdminRechargeProductsResponse + 155, // 367: hyapp.wallet.v1.WalletService.CreateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse + 155, // 368: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse + 156, // 369: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:output_type -> hyapp.wallet.v1.DeleteRechargeProductResponse + 160, // 370: hyapp.wallet.v1.WalletService.ListThirdPartyPaymentChannels:output_type -> hyapp.wallet.v1.ListThirdPartyPaymentChannelsResponse + 162, // 371: hyapp.wallet.v1.WalletService.SetThirdPartyPaymentMethodStatus:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse + 162, // 372: hyapp.wallet.v1.WalletService.UpdateThirdPartyPaymentRate:output_type -> hyapp.wallet.v1.ThirdPartyPaymentMethodResponse + 166, // 373: hyapp.wallet.v1.WalletService.SyncThirdPartyPaymentMethods:output_type -> hyapp.wallet.v1.SyncThirdPartyPaymentMethodsResponse + 168, // 374: hyapp.wallet.v1.WalletService.ListH5RechargeOptions:output_type -> hyapp.wallet.v1.H5RechargeOptionsResponse + 177, // 375: hyapp.wallet.v1.WalletService.CreateH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 177, // 376: hyapp.wallet.v1.WalletService.CreateTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 177, // 377: hyapp.wallet.v1.WalletService.GetTemporaryRechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 174, // 378: hyapp.wallet.v1.WalletService.ListTemporaryRechargeOrders:output_type -> hyapp.wallet.v1.ListTemporaryRechargeOrdersResponse + 177, // 379: hyapp.wallet.v1.WalletService.SubmitH5RechargeTx:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 177, // 380: hyapp.wallet.v1.WalletService.GetH5RechargeOrder:output_type -> hyapp.wallet.v1.H5RechargeOrderResponse + 179, // 381: hyapp.wallet.v1.WalletService.VerifyCoinSellerRechargeReceipt:output_type -> hyapp.wallet.v1.VerifyCoinSellerRechargeReceiptResponse + 181, // 382: hyapp.wallet.v1.WalletService.HandleMifapayNotify:output_type -> hyapp.wallet.v1.HandleMifapayNotifyResponse + 183, // 383: hyapp.wallet.v1.WalletService.HandleV5PayNotify:output_type -> hyapp.wallet.v1.HandleV5PayNotifyResponse + 186, // 384: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:output_type -> hyapp.wallet.v1.GetDiamondExchangeConfigResponse + 189, // 385: hyapp.wallet.v1.WalletService.ListWalletTransactions:output_type -> hyapp.wallet.v1.ListWalletTransactionsResponse + 199, // 386: hyapp.wallet.v1.WalletService.ListVipPackages:output_type -> hyapp.wallet.v1.ListVipPackagesResponse + 211, // 387: hyapp.wallet.v1.WalletService.GetVipProgramConfig:output_type -> hyapp.wallet.v1.GetVipProgramConfigResponse + 201, // 388: hyapp.wallet.v1.WalletService.GetMyVip:output_type -> hyapp.wallet.v1.GetMyVipResponse + 233, // 389: hyapp.wallet.v1.WalletService.CheckVipBenefit:output_type -> hyapp.wallet.v1.CheckVipBenefitResponse + 203, // 390: hyapp.wallet.v1.WalletService.PurchaseVip:output_type -> hyapp.wallet.v1.PurchaseVipResponse + 205, // 391: hyapp.wallet.v1.WalletService.DebitCPBreakupFee:output_type -> hyapp.wallet.v1.DebitCPBreakupFeeResponse + 207, // 392: hyapp.wallet.v1.WalletService.DebitWheelDraw:output_type -> hyapp.wallet.v1.DebitWheelDrawResponse + 209, // 393: hyapp.wallet.v1.WalletService.GrantVip:output_type -> hyapp.wallet.v1.GrantVipResponse + 227, // 394: hyapp.wallet.v1.WalletService.GrantVipTrialCard:output_type -> hyapp.wallet.v1.GrantVipTrialCardResponse + 229, // 395: hyapp.wallet.v1.WalletService.EquipVipTrialCard:output_type -> hyapp.wallet.v1.EquipVipTrialCardResponse + 231, // 396: hyapp.wallet.v1.WalletService.UnequipVipTrialCard:output_type -> hyapp.wallet.v1.UnequipVipTrialCardResponse + 236, // 397: hyapp.wallet.v1.WalletService.ListAdminVipLevels:output_type -> hyapp.wallet.v1.ListAdminVipLevelsResponse + 238, // 398: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:output_type -> hyapp.wallet.v1.UpdateAdminVipLevelsResponse + 213, // 399: hyapp.wallet.v1.WalletService.UpdateAdminVipProgramConfig:output_type -> hyapp.wallet.v1.UpdateAdminVipProgramConfigResponse + 215, // 400: hyapp.wallet.v1.WalletService.UpdateMyVipSettings:output_type -> hyapp.wallet.v1.UpdateMyVipSettingsResponse + 221, // 401: hyapp.wallet.v1.WalletService.GetMyCurrentVipDailyCoinRebate:output_type -> hyapp.wallet.v1.GetMyCurrentVipDailyCoinRebateResponse + 223, // 402: hyapp.wallet.v1.WalletService.ListMyVipDailyCoinRebateStatuses:output_type -> hyapp.wallet.v1.ListMyVipDailyCoinRebateStatusesResponse + 225, // 403: hyapp.wallet.v1.WalletService.ClaimVipDailyCoinRebate:output_type -> hyapp.wallet.v1.ClaimVipDailyCoinRebateResponse + 240, // 404: hyapp.wallet.v1.WalletService.CreditTaskReward:output_type -> hyapp.wallet.v1.CreditTaskRewardResponse + 242, // 405: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:output_type -> hyapp.wallet.v1.CreditLuckyGiftRewardResponse + 244, // 406: hyapp.wallet.v1.WalletService.CreditWheelReward:output_type -> hyapp.wallet.v1.CreditWheelRewardResponse + 246, // 407: hyapp.wallet.v1.WalletService.CreditRoomTurnoverReward:output_type -> hyapp.wallet.v1.CreditRoomTurnoverRewardResponse + 248, // 408: hyapp.wallet.v1.WalletService.CreditInviteActivityReward:output_type -> hyapp.wallet.v1.CreditInviteActivityRewardResponse + 250, // 409: hyapp.wallet.v1.WalletService.CreditAgencyOpeningReward:output_type -> hyapp.wallet.v1.CreditAgencyOpeningRewardResponse + 252, // 410: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:output_type -> hyapp.wallet.v1.ApplyGameCoinChangeResponse + 257, // 411: hyapp.wallet.v1.WalletService.GetRedPacketConfig:output_type -> hyapp.wallet.v1.GetRedPacketConfigResponse + 259, // 412: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:output_type -> hyapp.wallet.v1.UpdateRedPacketConfigResponse + 261, // 413: hyapp.wallet.v1.WalletService.CreateRedPacket:output_type -> hyapp.wallet.v1.CreateRedPacketResponse + 263, // 414: hyapp.wallet.v1.WalletService.ClaimRedPacket:output_type -> hyapp.wallet.v1.ClaimRedPacketResponse + 265, // 415: hyapp.wallet.v1.WalletService.ListRedPackets:output_type -> hyapp.wallet.v1.ListRedPacketsResponse + 267, // 416: hyapp.wallet.v1.WalletService.GetRedPacket:output_type -> hyapp.wallet.v1.GetRedPacketResponse + 269, // 417: hyapp.wallet.v1.WalletService.ExpireRedPackets:output_type -> hyapp.wallet.v1.ExpireRedPacketsResponse + 271, // 418: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:output_type -> hyapp.wallet.v1.RetryRedPacketRefundResponse + 288, // [288:419] is the sub-list for method output_type + 157, // [157:288] is the sub-list for method input_type + 157, // [157:157] is the sub-list for extension type_name + 157, // [157:157] is the sub-list for extension extendee + 0, // [0:157] is the sub-list for field type_name } func init() { file_proto_wallet_v1_wallet_proto_init() } diff --git a/api/proto/wallet/v1/wallet.proto b/api/proto/wallet/v1/wallet.proto index a81f76c6..1f78c2ef 100644 --- a/api/proto/wallet/v1/wallet.proto +++ b/api/proto/wallet/v1/wallet.proto @@ -1139,6 +1139,8 @@ message EquipUserResourceRequest { int64 user_id = 3; int64 resource_id = 4; string entitlement_id = 5; + // command_id 是装备状态变更的业务幂等键;request_id 只用于链路追踪。 + string command_id = 6; } message EquipUserResourceResponse { @@ -2028,6 +2030,9 @@ message VipBenefit { int64 created_at_ms = 13; int64 updated_at_ms = 14; VipBenefitPresentation presentation = 15; + // resource 仅在 resource_id 指向同 App、active 且 resource_type 匹配的真实资源时返回。 + // 运营尚未配置素材的权益保留 resource_id/resource_type 事实,但该对象为空,客户端不得猜 URL。 + Resource resource = 16; } message VipLevel { @@ -2404,6 +2409,10 @@ message CheckVipBenefitResponse { VipState state = 3; string denial_reason = 4; int64 evaluated_at_ms = 5; + // required_level 是当前 App 所有 active 等级中首次显式配置该权益的等级;0 表示未配置。 + int32 required_level = 6; + // current_effective_level 来自 wallet 合并付费会员与体验卡后的最终状态。 + int32 current_effective_level = 7; } message AdminVipLevelInput { diff --git a/docs/flutter对接/Fami_VIP五阶段Flutter接口对接.md b/docs/flutter对接/Fami_VIP五阶段Flutter接口对接.md new file mode 100644 index 00000000..f9dabfad --- /dev/null +++ b/docs/flutter对接/Fami_VIP五阶段Flutter接口对接.md @@ -0,0 +1,1485 @@ +# Fami VIP 五阶段 Flutter 接口对接 + +本文是本次五阶段后端补齐后的 Flutter 执行契约,覆盖 VIP 素材、资源佩戴、房间背景、房间装扮、主页隐私、房间图片消息和腾讯云 IM。字段以 gateway 当前 HTTP DTO 与 room-service 当前 IM 映射为准。 + +## 1. 适用边界 + +- Fami 当前 VIP 体系是 `tiered_privilege_v1`。资格只能读取服务端返回的 `state.effective_benefits`,执行写操作时仍由后端实时复验。 +- Lalu 当前保持 `legacy_timed`。旧版房间背景 `image_url` 请求和不带资源佩戴 `command_id` 的客户端仍兼容;新 Flutter 应统一使用本文新协议。 +- Flutter 不按 VIP 等级、`benefit_code` 或 `resource_code` 拼接素材 URL。只有接口实际返回的 `benefits[].resource`、背包 `resource` 或房间最终快照可以渲染。 +- 用户级装扮与房间级装扮分开:`mic_seat_animation` 走用户资源佩戴;`room_border` 和 `room_name_style` 必须走 Room Cell 的房间装扮接口。 +- 房间背景和房间图片只支持 JPEG、PNG、GIF、WebP。首版不支持 APNG、SVG、视频或外部 URL。 +- 房间状态以 HTTP 快照恢复,腾讯云 IM 只负责实时增量。不能把 IM 是否到达当成权益或持久状态事实。 + +五阶段对应关系: + +| 阶段 | Flutter 可用结果 | +| --- | --- | +| 1. 契约与错误 | VIP 套餐内嵌真实 `resource`;资源装备有 `command_id`;`VIP_BENEFIT_REQUIRED` 有结构化 `data` | +| 2. 资源映射与复验 | `mic_skin -> mic_seat_animation`、`room_border -> room_border`、`colored_room_name -> room_name_style`;写操作复验当前权益和 entitlement | +| 3. 背景与装扮 | 房间专用上传、完整媒体元数据、背景保存/切换、房间装扮 Room Cell 快照与 IM | +| 4. 主页隐私 | 目标统计隐藏显式返回;匿名访问在写入时固化,访客列表不泄露真实 ID | +| 5. 房间图片 | 专用上传、发言与权益门禁、command log、durable outbox、腾讯云 IM 图片事件 | + +## 2. 通用 HTTP 约定 + +### 2.1 Base URL + +```text +线上:https://api.global-interaction.com +本地:http://127.0.0.1:13000 +``` + +所有下文路径都拼接在同一 gateway Base URL 后。Flutter 从当前环境配置取得 host,不在业务代码写死域名。 + +### 2.2 Headers + +| Header | 必填 | 说明 | +| --- | --- | --- | +| `Authorization: Bearer {access_token}` | 是 | 当前登录用户 token | +| `Content-Type: application/json` | JSON 写接口 | 请求体为 JSON | +| `Content-Type: multipart/form-data` | 上传接口 | 包含 `command_id` 和 `file` | +| `X-App-Code` | 通常否 | 由 App 包和登录态决定;不得在同一 token 下任意切换 Fami/Lalu | + +客户端不提交 `app_code`、`user_id`、`actor_user_id`、`request_id`。这些值由 gateway 从登录态和请求上下文注入。 + +### 2.3 Envelope + +成功: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_xxx", + "data": {} +} +``` + +失败通常省略 `data`;需要客户端执行动作的错误会返回安全结构化 `data`。业务分支只判断 `code`,不要解析 `message`。日志至少记录 path、`code` 和 `request_id`,不能记录 token。 + +### 2.4 `command_id` + +- 最大 128 字节,建议 UUID v4 或 `功能前缀 + UUID`。 +- 它是业务幂等键,`request_id` 只是链路追踪,不能替代。 +- 同一次用户动作超时、断网或 5xx 重试必须复用同一个 `command_id` 和完全相同的业务 payload。 +- 同键同 payload 返回原结果;同键换 payload 返回 `IDEMPOTENCY_CONFLICT`。 +- 用户重新选择素材、重新点击形成新的业务动作时生成新键。 + +建议动作边界: + +| 动作 | command_id 使用范围 | +| --- | --- | +| 上传并保存一个房间背景 | upload 与 save 共用 A | +| 把已保存背景设为当前背景 | set 单独使用 B | +| 应用一个房间装扮 | apply 使用 C | +| 上传并发送一条房间图片 | upload 与 send 共用 D | +| 佩戴一个用户资源 | equip 使用 E | + +## 3. 接口总览 + +| 功能 | Method | Path | +| --- | --- | --- | +| 当前 VIP 最终态 | `GET` | `/api/v1/vip/me` | +| VIP 套餐与真实素材 | `GET` | `/api/v1/vip/packages` | +| VIP 功能开关 | `GET/PATCH` | `/api/v1/vip/settings` | +| 查询我的资源 | `GET` | `/api/v1/users/me/resources?resource_type={type}` | +| 佩戴用户级资源 | `POST` | `/api/v1/users/me/resources/{resource_id}/equip` | +| 查询我已佩戴外观 | `GET` | `/api/v1/users/me/appearance` | +| 查询其他用户已佩戴外观 | `GET` | `/api/v1/users/by-id/{user_id}/appearance` | +| 上传房间背景 | `POST multipart` | `/api/v1/rooms/{room_id}/backgrounds/upload` | +| 保存房间背景素材 | `POST` | `/api/v1/rooms/backgrounds/save` | +| 查询房间背景素材 | `GET` | `/api/v1/rooms/{room_id}/backgrounds` | +| 切换当前背景 | `POST` | `/api/v1/rooms/backgrounds/set` | +| 应用房间装扮 | `POST` | `/api/v1/rooms/{room_id}/decorations/apply` | +| 记录主页访问 | `POST` | `/api/v1/users/{user_id}/visit` | +| 查询访问我的用户 | `GET` | `/api/v1/users/me/visitors?page=1&page_size=20` | +| 上传房间图片 | `POST multipart` | `/api/v1/rooms/{room_id}/messages/images/upload` | +| 发送房间图片 | `POST` | `/api/v1/rooms/{room_id}/messages/image` | +| 断线后恢复房间最终状态 | `GET` | `/api/v1/rooms/{room_id}/detail` | + +已有 VIP 购买、体验卡、返现和上线通知接口保持不变: + +```text +POST /api/v1/vip/purchase +POST /api/v1/vip/trial-cards/{entitlement_id}/equip +DELETE /api/v1/vip/trial-cards/equipped +GET /api/v1/vip/coin-rebates/current +POST /api/v1/vip/coin-rebates/statuses +POST /api/v1/vip/coin-rebates/{rebate_id}/claim +POST /api/v1/vip/online-notice +``` + +## 4. VIP 套餐、最终态与真实资源 + +### 4.1 套餐接口 + +```http +GET /api/v1/vip/packages +Authorization: Bearer {access_token} +``` + +无 query 和 body 参数。代表响应: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_packages", + "data": { + "current_vip": { + "level": 5, + "name": "VIP5", + "active": true, + "started_at_ms": 1784000000000, + "expires_at_ms": 1786592000000, + "program_type": "tiered_privilege_v1", + "config_version": 12 + }, + "packages": [ + { + "level": 5, + "name": "VIP5", + "status": "active", + "price_coin": 5000, + "duration_ms": 2592000000, + "reward_resource_group_id": 0, + "reward_items": [], + "can_purchase": true, + "sort_order": 50, + "recharge_gate_required": false, + "required_recharge_coin_amount": 0, + "user_recharge_coin_amount": 0, + "purchase_locked_reason": "", + "benefits": [ + { + "benefit_code": "mic_skin", + "name": "VIP麦位皮肤", + "benefit_type": "decoration", + "unlock_level": 5, + "status": "active", + "trial_enabled": true, + "resource_id": 5105, + "resource_type": "mic_seat_animation", + "execution_scope": "room", + "auto_equip": false, + "sort_order": 50, + "metadata_json": "", + "created_at_ms": 1784000000000, + "updated_at_ms": 1784000000000, + "presentation": { + "description": "VIP麦位动态皮肤", + "preview_items": [], + "numeric_reward": null + }, + "resource": { + "resource_id": 5105, + "resource_code": "fami_vip5_mic", + "resource_type": "mic_seat_animation", + "name": "VIP5 Mic", + "status": "active", + "grantable": false, + "grant_strategy": "new_entitlement", + "usage_scopes": ["room_mic"], + "asset_url": "https://cdn.example.com/vip/v5-mic.svga", + "preview_url": "https://cdn.example.com/vip/v5-mic.png", + "animation_url": "https://cdn.example.com/vip/v5-mic.svga", + "format": "svga", + "metadata_json": "{\"format\":\"svga\"}", + "sort_order": 50, + "created_at_ms": 1784000000000, + "updated_at_ms": 1784000000000 + } + } + ], + "config_version": 12 + } + ], + "program_config": { + "app_code": "fami", + "program_type": "tiered_privilege_v1", + "level_count": 9, + "same_level_expiry_policy": "extend_remaining", + "upgrade_expiry_policy": "replace_from_now", + "downgrade_purchase_policy": "reject", + "benefit_inheritance_policy": "target_only", + "grant_mode": "trial_card", + "trial_card_enabled": true, + "status": "active", + "config_version": 12 + }, + "state": { + "paid_vip": { + "level": 5, + "name": "VIP5", + "active": true, + "started_at_ms": 1784000000000, + "expires_at_ms": 1786592000000, + "program_type": "tiered_privilege_v1", + "config_version": 12 + }, + "equipped_trial_card": null, + "effective_vip": { + "level": 5, + "name": "VIP5", + "active": true, + "started_at_ms": 1784000000000, + "expires_at_ms": 1786592000000, + "program_type": "tiered_privilege_v1", + "config_version": 12 + }, + "effective_source": "paid", + "effective_benefits": [ + { + "benefit_code": "mic_skin", + "name": "VIP麦位皮肤", + "benefit_type": "decoration", + "unlock_level": 5, + "status": "active", + "trial_enabled": true, + "resource_id": 5105, + "resource_type": "mic_seat_animation", + "execution_scope": "room", + "auto_equip": false, + "sort_order": 50, + "metadata_json": "", + "created_at_ms": 1784000000000, + "updated_at_ms": 1784000000000, + "presentation": { + "description": "VIP麦位动态皮肤", + "preview_items": [], + "numeric_reward": null + }, + "resource": { + "resource_id": 5105, + "resource_code": "fami_vip5_mic", + "resource_type": "mic_seat_animation", + "name": "VIP5 Mic", + "status": "active", + "grantable": false, + "grant_strategy": "new_entitlement", + "usage_scopes": ["room_mic"], + "asset_url": "https://cdn.example.com/vip/v5-mic.svga", + "preview_url": "https://cdn.example.com/vip/v5-mic.png", + "animation_url": "https://cdn.example.com/vip/v5-mic.svga", + "format": "svga", + "metadata_json": "{\"format\":\"svga\"}", + "sort_order": 50, + "created_at_ms": 1784000000000, + "updated_at_ms": 1784000000000 + } + } + ], + "evaluated_at_ms": 1784265600000, + "program_config": { + "app_code": "fami", + "program_type": "tiered_privilege_v1", + "config_version": 12 + }, + "user_settings": { + "app_code": "fami", + "user_id": 10001, + "room_entry_notice_enabled": true, + "online_global_notice_enabled": true, + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true, + "leaderboard_invisible_enabled": true, + "updated_at_ms": 0 + } + } + } +} +``` + +`benefits[].resource` 是可选字段,只有同时满足以下条件才出现: + +1. `resource_id > 0`; +2. 资源属于当前 App; +3. 资源 `status=active`; +4. 资源的 `resource_type` 与 benefit 精确一致。 + +条件不满足时字段会被完全省略,不会返回空对象。Flutter 展示素材入口应使用: + +```dart +final canRenderMaterial = benefit.status == 'active' && + benefit.resource != null && + benefit.resource!.status == 'active' && + benefit.resource!.resourceType == benefit.resourceType; +``` + +后台配置顺序是“先创建同 App 的真实 active resource,再把该 resource 绑定到 benefit”。真实素材不存在时,该 benefit 必须保持 `status=disabled`、`resource_id=0`,响应不返回 `resource`;不能先启用权益再让客户端使用占位素材。 + +`resource.format` 是客户端选择素材渲染器的唯一格式依据。后端优先取 `metadata_json.format`,缺省时兼容取 `metadata_json.animation_format`,并返回小写值;两者都没有时顶层 `format` 字段省略,Flutter 应当作“格式未配置/当前不支持”,不得根据 `asset_url`、`preview_url` 或 `animation_url` 后缀推断。后台创建 `mic_seat_animation` 或 `room_border` 资源时必须在 `metadata_json.format` 中选择 `png/jpeg/webp/gif/svga/pag/mp4` 之一。 + +当前映射: + +| benefit_code | resource_type | 应用位置 | +| --- | --- | --- | +| `mic_skin` | `mic_seat_animation` | 用户资源 equip 后由 appearance/房间资料投影 | +| `room_border` | `room_border` | 房间 decoration apply | +| `colored_room_name` | `room_name_style` | 房间 decoration apply,`decoration_type=colored_room_name` | + +本版本初始数据只有 Fami VIP4–VIP8 的 `mic_skin` 已绑定可核实资源;`room_border`、`colored_room_name` 和 VIP9 mic 素材当前为 disabled/无 `resource`。Flutter 必须隐藏素材操作,等待后台配置真实资源,不能放占位 URL。 + +### 4.2 当前 VIP 状态 + +```http +GET /api/v1/vip/me +``` + +返回根层兼容字段、`program_config` 和完整 `state`。授权只看: + +```text +data.state.effective_vip +data.state.effective_benefits +data.state.user_settings +``` + +不要使用根层 `data.level` 重新推导权益。登录、切账号、冷启动、回前台、购买/体验卡切换成功以及收到 VIP 状态类 IM 后重新拉取。 + +### 4.3 VIP 设置 + +```http +GET /api/v1/vip/settings +PATCH /api/v1/vip/settings +``` + +PATCH 是局部更新,至少提交一个字段;显式 `false` 不能当成未提交: + +```json +{ + "hide_profile_data_enabled": true, + "anonymous_profile_visit_enabled": true +} +``` + +开关只是用户偏好,不能单独授权。执行主页访问、房间行为或上线通知时,后端会把当前有效 benefit 与对应开关合并判定。 + +## 5. 用户资源查询与幂等佩戴 + +### 5.1 查询背包资源 + +```http +GET /api/v1/users/me/resources?resource_type=mic_seat_animation +``` + +`resource_type` 可省略。房间装扮可分别查询 `room_border`、`room_name_style`,但不能调用通用 equip;它们必须走第 7 节 Room Cell 接口。 + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_resources", + "data": { + "items": [ + { + "entitlement_id": "ent_abc", + "resource_id": 5105, + "resource": { + "resource_id": 5105, + "resource_code": "fami_vip5_mic", + "resource_type": "mic_seat_animation", + "name": "VIP5 Mic", + "status": "active", + "grantable": false, + "grant_strategy": "new_entitlement", + "usage_scopes": ["room_mic"], + "asset_url": "https://cdn.example.com/vip/v5-mic.svga", + "preview_url": "https://cdn.example.com/vip/v5-mic.png", + "animation_url": "https://cdn.example.com/vip/v5-mic.svga", + "format": "svga", + "metadata_json": "{\"format\":\"svga\"}", + "sort_order": 50, + "created_at_ms": 1784000000000, + "updated_at_ms": 1784000000000 + }, + "status": "active", + "quantity": 1, + "remaining_quantity": 1, + "effective_at_ms": 1784000000000, + "expires_at_ms": 1786592000000, + "source_grant_id": "grant_abc", + "created_at_ms": 1784000000000, + "updated_at_ms": 1784000000000, + "equipped": false + } + ], + "total": 1 + } +} +``` + +### 5.2 佩戴用户级资源 + +```http +POST /api/v1/users/me/resources/5105/equip +Content-Type: application/json + +{ + "command_id": "equip_550e8400-e29b-41d4-a716-446655440000", + "entitlement_id": "ent_abc" +} +``` + +| 参数 | 必填 | 说明 | +| --- | --- | --- | +| path `resource_id` | 是 | 目标资源 ID | +| `command_id` | Fami 是 | 同一佩戴动作重试复用,最大 128 字节 | +| `entitlement_id` | 否 | 指定背包实例;省略时由服务端选择当前有效实例 | + +成功 `data` 就是一条完整背包资源对象,与上一节 `items[]` 同构,且 `equipped=true`。 + +服务端在写入前重新检查:entitlement 仍 active、未过期、有剩余数量、资源仍 active,以及 VIP 绑定资源的对应 benefit 仍有效。用户 VIP 到期后,即使历史 entitlement 仍存在,也不能重新佩戴该 VIP 资源。 + +资源装备不会直接给 Flutter 发腾讯云 IM;wallet 只发布内部 `UserResourceChanged`。用户级外观通过后续 `/users/me/appearance`、房间资料投影或重新进房刷新。 + +### 5.3 佩戴后的外观与房间投影 + +当前用户佩戴态: + +```http +GET /api/v1/users/me/appearance +Authorization: Bearer {access_token} +``` + +查询其他用户的公开外观使用 `GET /api/v1/users/by-id/{user_id}/appearance`。两个接口的 `data.mic_seat_animation` 同构。下例只展开麦位动效相关字段,其他 appearance 字段以 OpenAPI 为准: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_appearance", + "data": { + "user_id": "10001", + "mic_seat_animation": { + "resource_id": 5105, + "resource_code": "fami_vip5_mic", + "resource_type": "mic_seat_animation", + "name": "VIP5 Mic", + "asset_url": "https://cdn.example.com/vip/v5-mic.svga", + "preview_url": "https://cdn.example.com/vip/v5-mic.png", + "animation_url": "https://cdn.example.com/vip/v5-mic.svga", + "format": "svga", + "metadata_json": "{\"format\":\"svga\"}", + "entitlement_id": "ent_abc", + "expires_at_ms": 1786592000000 + }, + "resources": [ + { + "resource_id": 5105, + "resource_code": "fami_vip5_mic", + "resource_type": "mic_seat_animation", + "name": "VIP5 Mic", + "asset_url": "https://cdn.example.com/vip/v5-mic.svga", + "preview_url": "https://cdn.example.com/vip/v5-mic.png", + "animation_url": "https://cdn.example.com/vip/v5-mic.svga", + "format": "svga", + "metadata_json": "{\"format\":\"svga\"}", + "entitlement_id": "ent_abc", + "expires_at_ms": 1786592000000 + } + ], + "server_time_ms": 1784265600000 + } +} +``` + +没有佩戴时 `mic_seat_animation={}`。已佩戴但后台没有显式格式时,该对象仍存在但 `format` 字段省略,Flutter 不渲染且不从 URL 推断。房间 join/detail 等响应的 `data.profiles[].mic_seat_animation` 与此对象同构,其他用户的麦位效果必须直接使用该快照中的 `format + animation_url/asset_url`。 + +## 6. 房间背景:上传、保存、切换 + +### 6.1 专用上传 + +```http +POST /api/v1/rooms/{room_id}/backgrounds/upload +Authorization: Bearer {access_token} +Content-Type: multipart/form-data + +command_id={本次上传并保存动作的幂等键} +file={binary} +``` + +服务端同时校验本地文件事实、房主身份、房间 active 和 `custom_room_background` 权益,再写对象存储。 + +| 约束 | 房间背景 | +| --- | --- | +| 格式 | JPEG、PNG、GIF、WebP;文件签名、multipart MIME、文件扩展名必须一致 | +| 文件大小 | 最大 12 MiB | +| 宽高 | `width <= 2160`、`height <= 3840` | +| 总像素 | 最大 8,294,400 | +| 动画 | 仅 GIF/WebP;最多 120 帧,时长 `(0, 15000]ms` | +| 静态图元数据 | `frame_count=1`、`animated=false`、`duration_ms=0` | + +Flutter multipart 必须给 part 设置正确 MIME,例如 JPEG 使用 `image/jpeg`。不要把所有文件统一发成 `application/octet-stream`。 + +成功响应: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_bg_upload", + "data": { + "purpose": "room_background", + "room_id": "fami_room_1001", + "command_id": "bg_550e8400-e29b-41d4-a716-446655440000", + "room_version": 17, + "media": { + "url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "object_key": "room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "content_type": "image/png", + "format": "png", + "size_bytes": 345678, + "width": 1080, + "height": 1920, + "animated": false, + "frame_count": 1, + "duration_ms": 0, + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "status": "active" + }, + "server_time_ms": 1784265600000 + } +} +``` + +`room_version` 是上传授权时看到的版本,不表示背景已保存或切换。Flutter 必须把整个 `media` 原样保留,不能只保存 URL,也不能改 `object_key`、尺寸、哈希或状态。 + +上传超时重试必须复用同一 `command_id` 和同一文件字节;换文件属于新动作,必须生成新 command。若后续 save 使用同一 command 提交不同媒体,返回 409 `IDEMPOTENCY_CONFLICT`。 + +当前后端不生成 poster/thumbnail,HTTP 中空值会省略 `poster_url`、`thumbnail_url`;IM 为保持固定媒体属性会返回这两个 key,当前值为空字符串。Flutter 不得自行拼接缩略图 URL。 + +### 6.2 保存素材 + +```http +POST /api/v1/rooms/backgrounds/save +Content-Type: application/json +``` + +Fami 请求: + +```json +{ + "room_id": "fami_room_1001", + "command_id": "bg_550e8400-e29b-41d4-a716-446655440000", + "media": { + "url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "object_key": "room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "content_type": "image/png", + "format": "png", + "size_bytes": 345678, + "width": 1080, + "height": 1920, + "animated": false, + "frame_count": 1, + "duration_ms": 0, + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "status": "active" + } +} +``` + +`image_url` 可省略,gateway 会从 `media.url` 补齐。Lalu 旧请求仍可只传 `room_id + image_url`。历史 Lalu 背景的列表/当前态和 IM 事件可能只有 `image_url` 或 `room_background_url`,没有嵌套 `media` 及其属性;Flutter 需兼容这种缺省,Fami 新链路则以完整 `media` 为准。 + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_bg_save", + "data": { + "background": { + "background_id": 101, + "room_id": "fami_room_1001", + "image_url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "created_by_user_id": "10001", + "created_at_ms": 1784265600100, + "updated_at_ms": 1784265600100, + "command_id": "bg_550e8400-e29b-41d4-a716-446655440000", + "media": { + "url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "object_key": "room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "content_type": "image/png", + "format": "png", + "size_bytes": 345678, + "width": 1080, + "height": 1920, + "animated": false, + "frame_count": 1, + "duration_ms": 0, + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "status": "active" + } + }, + "server_time_ms": 1784265600100 + } +} +``` + +保存只写房间素材列表,不改变 Room Cell,不发 IM。 + +### 6.3 查询素材 + +```http +GET /api/v1/rooms/fami_room_1001/backgrounds +``` + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_bg_list", + "data": { + "backgrounds": [ + { + "background_id": 101, + "room_id": "fami_room_1001", + "image_url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "created_by_user_id": "10001", + "created_at_ms": 1784265600100, + "updated_at_ms": 1784265600100, + "command_id": "bg_550e8400-e29b-41d4-a716-446655440000", + "media": { + "url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "object_key": "room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "content_type": "image/png", + "format": "png", + "size_bytes": 345678, + "width": 1080, + "height": 1920, + "animated": false, + "frame_count": 1, + "duration_ms": 0, + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "status": "active" + } + } + ], + "selected_background_url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "selected_background": { + "background_id": 101, + "room_id": "fami_room_1001", + "image_url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "created_by_user_id": "10001", + "created_at_ms": 1784265600100, + "updated_at_ms": 1784265600100, + "command_id": "bg_550e8400-e29b-41d4-a716-446655440000", + "media": { + "url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "object_key": "room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "content_type": "image/png", + "format": "png", + "size_bytes": 345678, + "width": 1080, + "height": 1920, + "animated": false, + "frame_count": 1, + "duration_ms": 0, + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "status": "active" + } + }, + "server_time_ms": 1784265600200 + } +} +``` + +未设置背景时 `selected_background_url` 和 `selected_background` 会省略。 + +### 6.4 切换当前背景 + +```http +POST /api/v1/rooms/backgrounds/set +Content-Type: application/json + +{ + "room_id": "fami_room_1001", + "command_id": "bg_set_550e8400-e29b-41d4-a716-446655440001", + "background_id": "101" +} +``` + +`background_id` 接受 JSON 数字或数字字符串。代表响应: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_bg_set", + "data": { + "result": { + "applied": true, + "room_version": 18, + "server_time_ms": 1784265600300 + }, + "room": { + "room_id": "fami_room_1001", + "im_group_id": "fami_room_1001", + "title": "Fami Room", + "background_url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "owner_user_id": "10001", + "status": "active", + "chat_enabled": true, + "locked": false, + "version": 18, + "active_background": { + "background_id": 101, + "room_id": "fami_room_1001", + "image_url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "created_by_user_id": "10001", + "created_at_ms": 1784265600100, + "updated_at_ms": 1784265600100, + "command_id": "bg_550e8400-e29b-41d4-a716-446655440000", + "media": { + "url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "object_key": "room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "content_type": "image/png", + "format": "png", + "size_bytes": 345678, + "width": 1080, + "height": 1920, + "animated": false, + "frame_count": 1, + "duration_ms": 0, + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "status": "active" + } + } + }, + "background": { + "background_id": 101, + "room_id": "fami_room_1001", + "image_url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "created_by_user_id": "10001", + "created_at_ms": 1784265600100, + "updated_at_ms": 1784265600100, + "command_id": "bg_550e8400-e29b-41d4-a716-446655440000", + "media": { + "url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "object_key": "room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "content_type": "image/png", + "format": "png", + "size_bytes": 345678, + "width": 1080, + "height": 1920, + "animated": false, + "frame_count": 1, + "duration_ms": 0, + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + "status": "active" + } + }, + "server_time_ms": 1784265600300 + } +} +``` + +### 6.5 背景 IM + +腾讯云原始 `TIMCustomElem`: + +| 字段 | 精确值 | +| --- | --- | +| `Ext` | `room_system_message` | +| `Desc` | `room_background_changed` | +| `Data` | 下方 JSON 的 UTF-8 字符串 | +| `CloudCustomData` | 与 `Data` 完全相同 | + +`Data` 同时保留 `attributes`,并把所有 attributes 平铺到顶层。媒体属性在 IM 中都是字符串: + +```json +{ + "event_id": "evt_xxx", + "room_id": "fami_room_1001", + "event_type": "room_background_changed", + "actor_user_id": 10001, + "room_version": 18, + "attributes": { + "background_id": "101", + "room_background_url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "object_key": "room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "content_type": "image/png", + "format": "png", + "size_bytes": "345678", + "width": "1080", + "height": "1920", + "animated": "false", + "frame_count": "1", + "duration_ms": "0", + "poster_url": "", + "thumbnail_url": "", + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + }, + "background_id": "101", + "room_background_url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "url": "https://cdn.example.com/room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "object_key": "room-media/fami/backgrounds/10001/fami_room_1001/x.png", + "content_type": "image/png", + "format": "png", + "size_bytes": "345678", + "width": "1080", + "height": "1920", + "animated": "false", + "frame_count": "1", + "duration_ms": "0", + "poster_url": "", + "thumbnail_url": "", + "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" +} +``` + +状态处理:同房间、`event_id` 未处理且 `room_version >= localRoomVersion` 才应用;如果版本跳跃大于 1,先拉 `/rooms/{room_id}/detail`。断线重进始终使用详情中的 `room.active_background` 恢复,不依赖历史 IM。 + +## 7. 房间边框与彩色房名 + +先从背包查询对应资源: + +```http +GET /api/v1/users/me/resources?resource_type=room_border +GET /api/v1/users/me/resources?resource_type=room_name_style +``` + +然后由房主应用到具体房间: + +```http +POST /api/v1/rooms/fami_room_1001/decorations/apply +Content-Type: application/json + +{ + "command_id": "room_decor_550e8400-e29b-41d4-a716-446655440002", + "decoration_type": "colored_room_name", + "resource_id": "5205", + "entitlement_id": "ent_room_name_abc" +} +``` + +| 字段 | 必填 | 说明 | +| --- | --- | --- | +| `command_id` | 是 | 最大 128 字节,重试复用 | +| `decoration_type` | 是 | `room_border` 或 `colored_room_name` | +| `resource_id` | 是 | JSON 数字或数字字符串 | +| `entitlement_id` | 否 | 建议明确传实例;省略时服务端选择当前有效实例 | + +`colored_room_name` 的资源目录类型固定为 `room_name_style`。成功响应: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_decor_apply", + "data": { + "result": { + "applied": true, + "room_version": 19, + "server_time_ms": 1784265600400 + }, + "room": { + "room_id": "fami_room_1001", + "im_group_id": "fami_room_1001", + "title": "Fami Room", + "owner_user_id": "10001", + "status": "active", + "chat_enabled": true, + "locked": false, + "version": 19, + "room_name_style": { + "resource_id": 5205, + "resource_code": "fami_vip_room_name_gold", + "resource_type": "room_name_style", + "name": "Gold Room Name", + "asset_url": "https://cdn.example.com/vip/room-name.json", + "preview_url": "https://cdn.example.com/vip/room-name.png", + "metadata_json": "{\"text_colors\":[\"#FFD76A\",\"#FF8A00\"],\"stops\":[0,1],\"angle_degrees\":90}", + "entitlement_id": "ent_room_name_abc", + "text_colors": ["#FFD76A", "#FF8A00"], + "stops": [0, 1], + "angle_degrees": 90, + "expires_at_ms": 1786592000000 + } + }, + "decoration_type": "colored_room_name", + "resource": { + "resource_id": 5205, + "resource_code": "fami_vip_room_name_gold", + "resource_type": "room_name_style", + "name": "Gold Room Name", + "asset_url": "https://cdn.example.com/vip/room-name.json", + "preview_url": "https://cdn.example.com/vip/room-name.png", + "metadata_json": "{\"text_colors\":[\"#FFD76A\",\"#FF8A00\"],\"stops\":[0,1],\"angle_degrees\":90}", + "entitlement_id": "ent_room_name_abc", + "text_colors": ["#FFD76A", "#FF8A00"], + "stops": [0, 1], + "angle_degrees": 90, + "expires_at_ms": 1786592000000 + }, + "server_time_ms": 1784265600400 + } +} +``` + +服务端必须同时验证:当前用户是房主、房间 active、VIP benefit 精确绑定同一 `resource_id/resource_type`、entitlement 有效、资源 active。`expires_at_ms` 是 VIP 与 entitlement 到期时间的较早者。 + +彩色房名 `text_colors` 最多 8 个,颜色只接受 `#RRGGBB` 或 `#RRGGBBAA`;`stops` 若提供,数量必须与颜色一致、范围 `[0,1]` 且升序。Flutter 直接渲染响应字段,不再次解析 `metadata_json` 作为权限事实。 + +房间列表、feed、详情和命令响应会直接返回最终 `room_border`、`room_name_style`;字段不存在表示当前无有效装扮。到达 `expires_at_ms` 时客户端立即隐藏,并刷新房间详情。 + +### 7.1 装扮 IM + +| TIMCustomElem 字段 | 精确值 | +| --- | --- | +| `Ext` | `room_system_message` | +| `Desc` | `room_decoration_changed` | +| `Data` | 下方 JSON 字符串 | +| `CloudCustomData` | 与 `Data` 相同 | + +```json +{ + "event_id": "evt_xxx", + "room_id": "fami_room_1001", + "event_type": "room_decoration_changed", + "actor_user_id": 10001, + "room_version": 19, + "attributes": { + "decoration_type": "colored_room_name", + "resource_id": "5205", + "resource_code": "fami_vip_room_name_gold", + "resource_type": "room_name_style", + "name": "Gold Room Name", + "asset_url": "https://cdn.example.com/vip/room-name.json", + "preview_url": "https://cdn.example.com/vip/room-name.png", + "animation_url": "", + "format": "", + "metadata_json": "{\"text_colors\":[\"#FFD76A\",\"#FF8A00\"],\"stops\":[0,1],\"angle_degrees\":90}", + "text_colors": "[\"#FFD76A\",\"#FF8A00\"]", + "stops": "[0,1]", + "angle_degrees": "90", + "entitlement_id": "ent_room_name_abc", + "expires_at_ms": "1786592000000" + }, + "decoration_type": "colored_room_name", + "resource_id": "5205", + "resource_code": "fami_vip_room_name_gold", + "resource_type": "room_name_style", + "name": "Gold Room Name", + "asset_url": "https://cdn.example.com/vip/room-name.json", + "preview_url": "https://cdn.example.com/vip/room-name.png", + "animation_url": "", + "format": "", + "metadata_json": "{\"text_colors\":[\"#FFD76A\",\"#FF8A00\"],\"stops\":[0,1],\"angle_degrees\":90}", + "text_colors": "[\"#FFD76A\",\"#FF8A00\"]", + "stops": "[0,1]", + "angle_degrees": "90", + "entitlement_id": "ent_room_name_abc", + "expires_at_ms": "1786592000000" +} +``` + +注意:IM 内 `text_colors` 和 `stops` 是“JSON 数组的字符串”,HTTP 中才是数组。Flutter IM parser 需要再做一次 `jsonDecode`。`room_name_style` 是结构化样式而不是媒体 renderer,HTTP 不返回 `format`,IM 的通用 attributes 中 `format` 为空字符串;客户端直接使用颜色、stops 和角度。 + +当后台禁用/改绑资源、entitlement 撤销、VIP 或等级配置失效时,wallet outbox 会驱动 Room Cell 清除已失效装扮。清除仍使用 `room_decoration_changed`:`decoration_type` 保留原槽位,`resource_id` 固定为字符串 `"0"`,其他资源 attributes 为空值。Flutter 收到后必须立即清空对应的 `room_border` 或 `room_name_style` 槽位,不能尝试查询资源 ID 0。状态事件统一按 `event_id + room_version` 去重和排序;纯自然到期不保证恰好有 IM,仍要按 `expires_at_ms` 本地到点隐藏并拉详情校准。 + +## 8. 主页数据隐藏与匿名访问 + +### 8.1 记录主页访问 + +```http +POST /api/v1/users/{target_user_id}/visit +Authorization: Bearer {access_token} +``` + +无 body。公开响应: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_visit_public", + "data": { + "recorded": true, + "target_stats_hidden": false, + "target_stats": { + "visitors_count": 20, + "following_count": 8, + "friends_count": 5, + "updated_at_ms": 1784265600500 + }, + "target_profile": { + "user_id": "20002", + "display_user_id": "888002", + "username": "Nina", + "avatar": "https://cdn.example.com/avatar.png" + } + } +} +``` + +目标用户的 `hide_profile_data` 权益和同名开关当前生效时: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_visit_hidden", + "data": { + "recorded": true, + "target_stats_hidden": true, + "target_profile": { + "user_id": "20002", + "display_user_id": "888002", + "username": "Nina", + "avatar": "https://cdn.example.com/avatar.png" + } + } +} +``` + +`target_stats_hidden=true` 时 `target_stats` 字段完全不存在,不会返回 `null` 或伪造的 0。Flutter 必须用显式布尔值控制统计区: + +```dart +if (data.targetStatsHidden) { + hideProfileStats(); +} else { + renderProfileStats(data.targetStats!); +} +``` + +`recorded=true` 表示首次形成该公开/匿名聚合项;同一身份重复访问会累计次数并更新时间,可能返回 `recorded=false`,不表示请求失败。 + +匿名与公开身份在“本次写入时”决定: + +- 访问者当时 `anonymous_profile_visit` 权益和开关都生效,写匿名表并固定随机 `anonymous_visit_id`。 +- 以后关闭开关或 VIP 到期,历史匿名记录仍保持匿名。 +- 同一真实用户先公开访问、后匿名访问,会形成两个独立展示聚合;历史公开记录不会改写,匿名记录也不会借公开记录泄露身份。 +- 匿名权益查询失败时本次访问不落库并返回上游错误;目标统计隐藏查询失败时按隐藏处理,避免泄漏。 +- 当前 App 访问链路不设好友或管理员白名单;除本人访问本身会直接被拒绝外,其他查看者一律以 `target_stats_hidden` 为最终结论。 + +### 8.2 查询我的访客 + +```http +GET /api/v1/users/me/visitors?page=1&page_size=20 +``` + +`page` 默认为 1,`page_size` 默认为 20,显式传值时两者都必须大于 0,且 `page` 和 `page_size` 都不能超过 100。超限时返回 HTTP 400 + `INVALID_ARGUMENT`,Flutter 应修正本地分页参数,不要当作空页或自动重试。这个边界把公开与匿名两个分支的单次合并窗口固定在最多各 10,000 条。 + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_visitors", + "data": { + "records": [ + { + "visitor_user_id": "30003", + "visitor_identity_hidden": false, + "target_user_id": "20002", + "visit_count": 4, + "last_visited_at_ms": 1784265600600 + }, + { + "anonymous_visit_id": "anon_0123456789abcdef0123456789abcdef", + "visitor_identity_hidden": true, + "target_user_id": "20002", + "visit_count": 2, + "last_visited_at_ms": 1784265500000 + } + ], + "total": 2 + } +} +``` + +解析规则: + +| `visitor_identity_hidden` | 必须存在 | 必须不存在 | UI | +| --- | --- | --- | --- | +| `false` | `visitor_user_id` | `anonymous_visit_id` | 可按 user_id 拉取公开资料 | +| `true` | `anonymous_visit_id` | `visitor_user_id` | 展示匿名头像/名称;不能发起资料查询 | + +`anonymous_visit_id` 是随机、目标用户作用域内稳定的展示 ID;不同被访问者看到的值不同,不能跨用户关联。主页访问和列表不发送 IM。 + +## 9. 房间图片消息 + +### 9.1 专用上传 + +```http +POST /api/v1/rooms/{room_id}/messages/images/upload +Authorization: Bearer {access_token} +Content-Type: multipart/form-data + +command_id={本次上传并发送动作的幂等键} +file={binary} +``` + +上传前服务端校验:房间 active、当前用户仍在房、未被 ban、未被禁言、公屏开启,以及 `room_image_message` 当前生效。用户不要求在麦位上。 + +| 约束 | 房间图片 | +| --- | --- | +| 格式 | JPEG、PNG、GIF、WebP;签名、MIME、扩展名一致 | +| 文件大小 | 最大 8 MiB | +| 宽高 | `width <= 4096`、`height <= 4096` | +| 总像素 | 最大 16,777,216 | +| 动画 | 仅 GIF/WebP;最多 120 帧,最长 15 秒 | + +成功响应与背景上传同构,但: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_image_upload", + "data": { + "purpose": "room_image", + "room_id": "fami_room_1001", + "command_id": "room_image_550e8400-e29b-41d4-a716-446655440003", + "room_version": 19, + "media": { + "url": "https://cdn.example.com/room-media/fami/images/10001/fami_room_1001/y.webp", + "object_key": "room-media/fami/images/10001/fami_room_1001/y.webp", + "content_type": "image/webp", + "format": "webp", + "size_bytes": 456789, + "width": 800, + "height": 800, + "animated": true, + "frame_count": 12, + "duration_ms": 2400, + "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + "status": "active" + }, + "server_time_ms": 1784265600700 + } +} +``` + +图片上传超时同样只能用原 `command_id + 同一文件` 重试;重新选图生成新 command。当前 `poster_url/thumbnail_url` 不生成,HTTP 为空时省略,IM 中以空字符串出现。 + +### 9.2 发送图片 + +```http +POST /api/v1/rooms/fami_room_1001/messages/image +Content-Type: application/json +``` + +```json +{ + "command_id": "room_image_550e8400-e29b-41d4-a716-446655440003", + "image": { + "url": "https://cdn.example.com/room-media/fami/images/10001/fami_room_1001/y.webp", + "object_key": "room-media/fami/images/10001/fami_room_1001/y.webp", + "content_type": "image/webp", + "format": "webp", + "size_bytes": 456789, + "width": 800, + "height": 800, + "animated": true, + "frame_count": 12, + "duration_ms": 2400, + "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + "status": "active" + } +} +``` + +必须原样使用上传响应的整个 `media`。成功响应: + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_image_send", + "data": { + "result": { + "applied": true, + "room_version": 20, + "server_time_ms": 1784265600800 + }, + "accepted": true, + "message_id": "room_msg_0123456789abcdef0123456789abcdef", + "room_id": "fami_room_1001", + "sender_user_id": "10001", + "server_time_ms": 1784265600800, + "image": { + "url": "https://cdn.example.com/room-media/fami/images/10001/fami_room_1001/y.webp", + "object_key": "room-media/fami/images/10001/fami_room_1001/y.webp", + "content_type": "image/webp", + "format": "webp", + "size_bytes": 456789, + "width": 800, + "height": 800, + "animated": true, + "frame_count": 12, + "duration_ms": 2400, + "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", + "status": "active" + } + } +} +``` + +`accepted=true` 表示 command log 与 durable outbox 已提交,不表示腾讯云 IM 已到达每个在线客户端。`message_id` 由 `app_code + room_id + command_id` 稳定生成;相同请求重放仍返回同一个值。 + +服务端对同一 command 只提交一条逻辑图片消息和一条 outbox 事实;补偿投递或 SDK 漫游仍可能让客户端观察到重复到达,所以 Flutter 用 `event_id` 去重,并以 `message_id` 作为跨投递/乐观消息合并兜底。 + +发送者可用 HTTP 响应先插入一条 `sending/pending` UI 消息,收到 IM 后按 `message_id` 合并成已确认状态,不能再插入第二条。 + +### 9.3 图片 IM + +| TIMCustomElem 字段 | 精确值 | +| --- | --- | +| `Ext` | `room_system_message` | +| `Desc` | `room_image_message` | +| `Data` | 下方 JSON 字符串 | +| `CloudCustomData` | 与 `Data` 相同 | + +```json +{ + "event_id": "evt_xxx", + "room_id": "fami_room_1001", + "event_type": "room_image_message", + "actor_user_id": 10001, + "room_version": 20, + "attributes": { + "message_id": "room_msg_0123456789abcdef0123456789abcdef", + "command_id": "room_image_550e8400-e29b-41d4-a716-446655440003", + "sender_user_id": "10001", + "image_url": "https://cdn.example.com/room-media/fami/images/10001/fami_room_1001/y.webp", + "image_object_key": "room-media/fami/images/10001/fami_room_1001/y.webp", + "image_content_type": "image/webp", + "image_format": "webp", + "image_size_bytes": "456789", + "image_width": "800", + "image_height": "800", + "image_animated": "true", + "image_frame_count": "12", + "image_duration_ms": "2400", + "image_poster_url": "", + "image_thumbnail_url": "", + "image_sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" + }, + "message_id": "room_msg_0123456789abcdef0123456789abcdef", + "command_id": "room_image_550e8400-e29b-41d4-a716-446655440003", + "sender_user_id": "10001", + "image_url": "https://cdn.example.com/room-media/fami/images/10001/fami_room_1001/y.webp", + "image_object_key": "room-media/fami/images/10001/fami_room_1001/y.webp", + "image_content_type": "image/webp", + "image_format": "webp", + "image_size_bytes": "456789", + "image_width": "800", + "image_height": "800", + "image_animated": "true", + "image_frame_count": "12", + "image_duration_ms": "2400", + "image_poster_url": "", + "image_thumbnail_url": "", + "image_sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789" +} +``` + +所有 `attributes`/平铺媒体值都是字符串。图片消息是内容流,不能因为 `room_version < localRoomVersion` 就丢弃;先按 `event_id` 去除 outbox 重投,再按 `message_id` 合并发送者的乐观消息或 SDK 重复消息。 + +## 10. `VIP_BENEFIT_REQUIRED` 结构化错误 + +资源佩戴、背景上传/保存/切换、房间装扮、房间图片上传/发送和上线通知都会在执行前复验权益。失败统一为: + +```http +HTTP/1.1 403 Forbidden +``` + +```json +{ + "code": "VIP_BENEFIT_REQUIRED", + "message": "vip benefit is required", + "request_id": "req_denied", + "data": { + "benefit_code": "room_image_message", + "required_level": 3, + "current_effective_level": 2, + "action": "send_room_image" + } +} +``` + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `benefit_code` | string | 本次缺失的精确权益 | +| `required_level` | int | 当前 App 首个显式配置该权益的 active 等级;`0` 表示当前未配置 | +| `current_effective_level` | int | 付费 VIP 与体验卡合并后的当前有效等级 | +| `action` | string | 被拒绝的后端动作。背景链路依次为 `upload_room_background`、`save_room_background`、`set_room_background`;其他动作包括 `equip_resource`、`apply_room_decoration`、`upload_room_image`、`send_room_image` | + +Flutter 处理:关闭 loading,保留用户尚未提交的本地选择,刷新 `/api/v1/vip/me` 和必要的 `/api/v1/vip/packages`,显示升级/续费入口。不要只修改本地等级后自动重试。 + +## 11. 其他错误矩阵 + +| HTTP | code | 场景 | Flutter 动作 | +| --- | --- | --- | --- | +| 400 | `INVALID_JSON` | JSON 无法解析 | 修正客户端序列化,不重试同一 body | +| 400 | `INVALID_ARGUMENT` | command、ID 或媒体元数据不完整 | 停止重试,重新构造请求 | +| 400 | `INVALID_MEDIA_DIMENSIONS` | 宽高、像素、帧数或动画时长越界 | 提示重新选择或在客户端压缩 | +| 401 | `UNAUTHORIZED` / token 类错误 | 登录态失效 | 走统一刷新 token/登录流程 | +| 403 | `PROFILE_REQUIRED` | 资料未完成 | 先完成资料 | +| 403 | `PERMISSION_DENIED` | 非房主、不在房、被 ban/禁言、对象不归属当前用户/房间 | 关闭对应操作并刷新房间状态 | +| 403 | `VIP_BENEFIT_REQUIRED` | 权益、开关或等级不满足 | 按第 10 节刷新 VIP | +| 404 | `NOT_FOUND` | 房间、背景、resource 或 entitlement 不存在 | 刷新对应列表 | +| 409 | `IDEMPOTENCY_CONFLICT` | 同 command_id 换了 payload | 客户端 bug;新动作生成新 ID,旧动作仍用旧 body | +| 409 | `ROOM_CLOSED` | 房间已关闭 | 退出房间并停止上传/发送 | +| 409 | `CONFLICT` | 记录匿名访问时的极低概率展示 ID 碰撞;事务已回滚 | 发起一次新的访问请求,让服务端重新生成随机 ID;不得回退为公开记录或在客户端构造匿名 ID | +| 409 | `CONFLICT` | 公屏关闭或当前状态不允许 | 刷新详情,不自动换 command 重试 | +| 413 | `FILE_TOO_LARGE` | 背景超过 12MiB、房间图片超过 8MiB | 客户端压缩或重选 | +| 415 | `UNSUPPORTED_MEDIA_TYPE` | 签名、MIME、扩展名不一致,或 APNG/其他格式 | 转码为受支持格式 | +| 500 | `INTERNAL_ERROR` | 服务端持久化或未分类内部错误 | 保留 `request_id` 上报;不得在本地伪造访问成功或隐私结论 | +| 502 | `UPSTREAM_ERROR` | owner service、wallet 或对象存储暂不可用 | 保留当前 command 与 payload,退避重试 | +| 503 | `UPSTREAM_ERROR` | 上传或房间 VIP 依赖未就绪 | 保留当前 command 与 payload,退避重试 | + +`FILE_TOO_LARGE`、`UNSUPPORTED_MEDIA_TYPE`、`INVALID_MEDIA_DIMENSIONS` 是上传专用 code。不要把所有失败都显示成“VIP不足”。 + +## 12. 腾讯云 IM 统一路由与去重 + +room-service 调腾讯云 REST 时使用以下原始形态;`CloudCustomData` 是消息级字段,不在 `TIMCustomElem.MsgContent` 内: + +```json +{ + "CloudCustomData": "{与 Data 相同的 JSON 字符串}", + "MsgBody": [ + { + "MsgType": "TIMCustomElem", + "MsgContent": { + "Data": "{业务 JSON 字符串}", + "Desc": "room_image_message", + "Ext": "room_system_message", + "Sound": "" + } + } + ] +} +``` + +Flutter SDK 中通常对应 `customElem.data/desc/extension`;具体 Dart 包字段名可以不同,但字节内容和值不能改写。 + +### 12.1 相关消息表 + +| 场景 | 会话 | Ext | Desc | +| --- | --- | --- | --- | +| VIP 激活 | C2C | `vip_notice` | `VipActivated` | +| VIP 进房展示 | 当前房间群 | `room_system_message` | `room_user_joined` | +| VIP 上线全服通知 | 全局播报群 | `im_broadcast` | `vip_online_notice` | +| 房间背景切换 | 当前房间群 | `room_system_message` | `room_background_changed` | +| 房间装扮切换 | 当前房间群 | `room_system_message` | `room_decoration_changed` | +| 房间图片 | 当前房间群 | `room_system_message` | `room_image_message` | + +用户资源 equip 没有客户端 IM。主页隐私写入也没有 IM。 +房间图片和房间装扮的客户端实时入口分别只有 `room_image_message`、`room_decoration_changed` 这两个 custom event;不要再等待 wallet/VIP 的第二条消息确认同一次房间动作。 + +### 12.2 Flutter 路由顺序 + +1. 从 `V2TimMessage.customElem` 读取 `extension/ext`、`desc`、`data`;SDK 字段名因封装不同,值必须与上表完全匹配。 +2. `Ext=room_system_message` 时解析 `Data` JSON。 +3. 验证 `data.event_type == Desc`、`data.room_id == currentRoomId`。 +4. 用进程内有界 LRU/TTL 集合按 `event_id` 去重;建议每个房间保留最近 1000 个或 30 分钟。 +5. 背景/装扮是状态事件:再按 `room_version` 排序,版本断层时拉详情。 +6. 图片是内容事件:按 `message_id` 再去重,不用本地较新的 room version 丢弃旧图片。 + +伪代码: + +```dart +void onCustomMessage(CustomElem elem) { + if (elem.extension != 'room_system_message') return; + final event = jsonDecode(utf8.decode(elem.data)) as Map; + if (event['event_type'] != elem.desc) return; + if (event['room_id'] != currentRoomId) return; + + final eventId = event['event_id'] as String? ?? ''; + if (eventId.isEmpty || handledEventIds.contains(eventId)) return; + handledEventIds.add(eventId); + + switch (event['event_type']) { + case 'room_background_changed': + applyBackgroundStateEvent(event); + break; + case 'room_decoration_changed': + applyDecorationStateEvent(event); + break; + case 'room_image_message': + mergeRoomImageByMessageId(event); + break; + } +} +``` + +腾讯云 IM 的 `Data` 和 `CloudCustomData` 内容相同;正常只消费一次 `customElem.data`。不要同时消费两份造成重复。 + +服务端群消息的腾讯 `Random` 已由 `event_id` 稳定派生,可收敛腾讯短窗口内的相同事件重试;超出腾讯去重窗口的 outbox 补偿仍可能再次到达,因此 Flutter 的 `event_id` 去重不可省略,图片再以 `message_id` 合并兜底。 + +### 12.3 刷新与恢复 + +| 时机 | HTTP 刷新 | +| --- | --- | +| 登录/切账号/冷启动/回前台 | `/vip/me` | +| 打开 VIP 中心 | 并发 `/vip/me` + `/vip/packages` | +| VIP 购买或体验卡成功 | 使用写响应更新;必要时再拉 `/vip/me` 和 `/vip/packages` | +| 用户资源佩戴成功 | 先用写响应更新背包,再拉 `/users/me/appearance`;已在房内时重拉 `/rooms/{room_id}/detail` 更新 `profiles[].mic_seat_animation` | +| 进入或重连房间 | `/rooms/{room_id}/detail` 初始化 `active_background`、`room_border`、`room_name_style` | +| 背景/装扮 IM 版本断层 | `/rooms/{room_id}/detail` | +| 装扮到 `expires_at_ms` | 本地立即隐藏,再拉详情确认 | +| 主页打开 | 先资料接口,再 `POST /users/{id}/visit`;按 `target_stats_hidden` 渲染 | +| 访客页打开/下拉刷新 | `/users/me/visitors` | + +房间恢复请求为 `GET /api/v1/rooms/{room_id}/detail`;`room_id` 是必填 path 参数,无 query 和 body。成功响应 `data` 字段如下: + +| 字段 | 含义 | +| --- | --- | +| `room` | 房间最终态,含可选 `active_background/room_border/room_name_style` | +| `viewer` / `permissions` / `is_followed` | 当前用户的房内身份、可操作权限与关注状态 | +| `seats` / `rank_top` / `online_count` / `profiles` | 麦位、首屏贡献榜、在线数和用户展示资料;`profiles` 可省略 | +| `im` / `rtc` | 腾讯 IM 群进入参数与 RTC 凭据状态 | +| `server_time_ms` | UTC Unix epoch milliseconds | + +不要持久化“拥有某权益”的最终判断。磁盘缓存最多用于首帧占位;所有敏感写操作都必须接受服务端实时拒绝。 + +## 13. Flutter 模块执行方案 + +建议按五个边界落地: + +1. `VipRepository` + - 解析 `/vip/me`、`/vip/packages`、资源列表和结构化 VIP 错误。 + - 以 `(appCode, configVersion)` 隔离缓存,不能把 Fami 权益合并到 Lalu。 +2. `AppearanceRepository` + - 解析当前/公开 appearance 以及房间 `profiles[]`中的同构资源外观。 + - 只根据显式 `format` 选择渲染器;字段缺省就不渲染,不从 URL 后缀推断。 +3. `RoomMediaRepository` + - 负责 MIME 检测、multipart 上传、保存/发送时原样回传 `RoomMedia`。 + - 一个 pending action 保存 `commandId + roomId + localFile + uploadedMedia`,超时重试不重建 command。 +4. `RoomStateStore` + - 保存 `roomVersion`、`activeBackground`、`roomBorder`、`roomNameStyle` 和用户资料 `profiles`。 + - HTTP 快照全量替换,IM 状态事件按 version 增量更新。 +5. `RoomCustomMessageRouter` + - 按 `Ext/Desc/event_type` 分发,维护 `event_id` LRU。 + - 图片维护独立 `message_id` 索引,用于发送者乐观消息与 IM 回执合并。 + +推荐上线顺序: + +1. 先发布兼容解析:允许 `resource`、`active_background`、`room_border`、`room_name_style` 缺省。 +2. 接入结构化 `VIP_BENEFIT_REQUIRED` 和统一 `command_id` 管理。 +3. 接入用户资源 equip 与 VIP 素材真实 URL。 +4. 接入背景专用上传、保存、切换及 IM。 +5. 接入装扮 HTTP/IM 与到期隐藏。 +6. 接入主页隐私两种分支。 +7. 最后接入房间图片乐观消息、outbox IM 去重和异常恢复。 + +## 14. 联调验收清单 + +- Fami `/vip/packages`:有真实资源时返回完整 `resource`,未配置时字段省略。 +- Lalu:旧背景 `image_url` 请求仍可用,新 Flutter 也可走专用协议。 +- 资源 equip:相同 command/body 重放成功;换 entitlement 返回 `IDEMPOTENCY_CONFLICT`。 +- 麦位动效:资源列表、appearance 和房间 `profiles[].mic_seat_animation` 的顶层 `format` 一致;后台未配格式时三处都省略该字段,Flutter 不从 URL 推断。 +- VIP 到期后尝试重新 equip 历史资源:返回带四个 data 字段的 `VIP_BENEFIT_REQUIRED`。 +- 背景:JPEG/PNG/GIF/WebP 正常;错误 MIME、APNG、超大小、超尺寸分别命中预期 code。 +- 背景 upload 成功但未 save:背景列表不增加;save 后未 set:当前背景不变化。 +- set 成功:HTTP、详情快照和 `room_background_changed` 三处一致。 +- 装扮:非房主、无 entitlement、资源类型错、VIP 到期都不能写入 Room Cell。 +- 彩色房名:HTTP 数组能渲染;IM 的数组字符串能二次解析。 +- 主页统计隐藏:`target_stats_hidden=true` 时 JSON 中没有 `target_stats`。 +- 匿名访问:访客列表没有真实 `visitor_user_id`;关闭匿名后历史记录仍匿名。 +- 同一用户公开后匿名:列表可出现公开和匿名两个独立聚合。 +- 房间图片:不在房、ban、禁言、公屏关闭、无权益均被拒绝;不要求上麦。 +- 图片 send HTTP 成功但 IM 重投:同一 `event_id/message_id` 只显示一条。 +- IM 状态版本断层、App 回前台或重连:详情快照可恢复最终背景和装扮。 diff --git a/docs/flutter对接/个人信息页用户上麦状态Flutter对接.md b/docs/flutter对接/个人信息页用户上麦状态Flutter对接.md new file mode 100644 index 00000000..ebfe5efa --- /dev/null +++ b/docs/flutter对接/个人信息页用户上麦状态Flutter对接.md @@ -0,0 +1,163 @@ +# 个人信息页用户上麦状态 Flutter 对接 + +## 1. 接口职责 + +个人信息页通过本接口判断被查看用户当前是否占用语音房麦位,并取得可直接交给统一进房入口的房间信息。 + +- 上麦事实只来自 `room-service` 当前 Room Cell 快照;不要用用户是否为房主、房间搜索结果、IM 本地状态或历史缓存推断。 +- 接口返回的是带时间戳的瞬时快照;点击 Live 后仍以 `POST /api/v1/rooms/join` 的实时校验为准。 +- 当前接口仅用于客态个人信息页。请求中包含当前登录用户本人时,该项固定返回 `not_on_mic`。 + +## 2. 请求 + +```http +GET /api/v1/users/voice-room-presence:batch?user_ids=163020,163021 +Authorization: Bearer {access_token} +``` + +| 参数 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `user_ids` | string | 是 | 逗号分隔的系统 `user_id`,不接受展示短号 | + +约束: + +- 去重后最多 100 个 ID,响应顺序按请求中第一次出现的顺序返回。 +- 空值、非数字、`0`、负数、空分段或超过上限都会整批返回 `INVALID_USER_IDS`。 +- `viewer_user_id`、`app_code` 和 `request_id` 由 gateway 从登录态和请求上下文注入,客户端不能提交。 +- 接口要求有效登录态且用户已完成资料。 + +## 3. 成功响应 + +```json +{ + "code": "OK", + "message": "ok", + "request_id": "req_xxx", + "data": { + "items": [ + { + "user_id": "163020", + "state": "on_mic", + "seat_no": 3, + "joinable": true, + "observed_at_ms": 1784265600000, + "room": { + "room_id": "100086", + "im_group_id": "100086", + "rtc_room_id": "100086", + "room_short_id": "886688", + "title": "Room name", + "owner_user_id": "12001", + "host_user_id": "12001", + "mode": "voice", + "cover_url": "https://cdn.example.com/room-cover.png", + "background_url": "https://cdn.example.com/room-bg.png", + "online_count": 56, + "seat_count": 10, + "locked": true + } + }, + { + "user_id": "163021", + "state": "not_on_mic", + "seat_no": 0, + "joinable": false, + "observed_at_ms": 1784265600000, + "room": null + } + ], + "server_time_ms": 1784265600123 + } +} +``` + +字段说明: + +| 字段 | 类型 | 说明 | +| --- | --- | --- | +| `items[].user_id` | string | 系统用户 ID;JSON 中按字符串返回 | +| `items[].state` | string | `on_mic` 或 `not_on_mic` | +| `items[].seat_no` | int | 当前麦位编号;未上麦为 `0` | +| `items[].joinable` | bool | 当前 viewer 是否允许通过该入口尝试进房 | +| `items[].observed_at_ms` | int64 | 本批状态完成判定时的 UTC epoch ms | +| `items[].room` | object/null | 仅 `on_mic && joinable` 时非空 | +| `server_time_ms` | int64 | 服务端本批响应时间,UTC epoch ms | + +房间字段可以直接构造现有 `VoiceRoomEntryArgs`: + +- `room_id` 是业务房间主键。 +- `im_group_id` 当前等于 `room_id`。 +- `rtc_room_id` 当前使用 TRTC string room ID,等于 `room_id`。 +- 当前房间模型没有独立主持人字段,`host_user_id` 与 `owner_user_id` 一致。 +- `locked=true` 表示密码房;点击后继续复用现有密码输入和进房流程,不能在个人页自行索取或缓存密码。 + +## 4. Flutter 展示和刷新规则 + +只有下面四个条件全部成立时显示 Live: + +```dart +final showLive = item.state == 'on_mic' && + item.joinable && + item.room != null && + item.room!.roomId.isNotEmpty; +``` + +点击时: + +1. 调用现有 `VoiceRoomNavigation.open()`。 +2. 用 `room` 构造 `VoiceRoomEntryArgs`。 +3. `entrySource` 使用 `profile_card_live_badge`。 +4. 正常调用 `POST /api/v1/rooms/join`;如果用户已经下麦但房间仍允许进入,可以正常进入。 +5. 进房返回房间关闭、封禁、密码错误等业务错误时,移除当前 Live 状态并按现有错误规则提示。 + +刷新时机: + +- 首次进入客态个人信息页。 +- App 从后台回到前台且个人信息页仍可见。 +- 从语音房返回个人信息页。 +- 用户手动刷新个人信息页。 + +不要轮询、持久化或使用上一次成功结果兜底。接口失败时只隐藏 Live,不影响个人信息页其他资料。 + +## 5. 后端状态边界 + +- `on_mic` 表示房间仍为 `active`、目标用户仍在房间在线集合中,并且占用一个当前有效麦位。`MicUp` 后的 `pending_publish` 和 RTC 已确认的 `publishing` 都属于已占麦。 +- 房主或管理员只有实际占用麦位时才算上麦;身份本身不算。 +- 同一 App 的 `room_user_presence` 使用 `(app_code, user_id)` 主键定位候选房间,最终状态再由 Room Cell 快照确认。 +- 跨 `app_code` 房间不会返回。 +- viewer 被目标房间封禁、房间已关闭、presence 已滞后或快照事实不完整时,统一返回 `not_on_mic + room:null`,不泄露旧房间 ID。 +- 密码房允许展示 Live,返回 `locked=true`,正式进房仍校验密码。 +- 当前房间产品模型没有“隐藏房/隐身上麦”字段,用户关系也没有资料位置黑名单能力;本版不做不存在的状态推断。以后增加此类产品能力时,必须在后端权限判断中收敛后再返回,Flutter 不得自行判断。 + +## 6. 错误响应 + +| HTTP | code | 处理方式 | +| --- | --- | --- | +| 400 | `INVALID_USER_IDS` | 修正参数;不要重试同一请求 | +| 401 | `UNAUTHORIZED` / `ACCESS_TOKEN_EXPIRED` | 走现有登录或刷新 token 流程 | +| 403 | `PROFILE_REQUIRED` | 先完成用户资料 | +| 503 | `VOICE_ROOM_PRESENCE_UNAVAILABLE` | 隐藏 Live;页面其他内容继续展示 | + +示例: + +```json +{ + "code": "INVALID_USER_IDS", + "message": "invalid user ids", + "request_id": "req_xxx" +} +``` + +排障日志只记录接口路径、目标 ID 数量、响应业务码和 `request_id`;不要记录 access token 或密码房密码。 + +## 7. 联调清单 + +- 未进房、只在房内未上麦:不显示 Live。 +- 在自己的房间或其他人的房间实际占麦:显示 Live,并返回实际所在房间。 +- `pending_publish`、`publishing`:都显示 Live。 +- 下麦、离房、被踢或关房后重新请求:返回 `not_on_mic`。 +- 密码房:显示 Live,点击后进入既有密码流程。 +- viewer 被目标房间封禁、跨 App、查询本人:不返回房间定位。 +- 一批中混合上麦、未上麦和重复用户:每个去重用户一条结果,顺序稳定。 +- 查询成功后立即下麦:点击仍以正式进房接口结果为准。 + diff --git a/docs/openapi/gateway.swagger.yaml b/docs/openapi/gateway.swagger.yaml index 9f42b175..4111e8b3 100644 --- a/docs/openapi/gateway.swagger.yaml +++ b/docs/openapi/gateway.swagger.yaml @@ -37,6 +37,8 @@ tags: description: 房间命令入口,最终由 room-service 执行。 - name: wallet description: App 钱包余额、币商转账和充值口径入口。 + - name: vip + description: App 维度 VIP 套餐、有效权益、体验卡和功能开关;Fami 使用 tiered_privilege_v1,Lalu 保持 legacy_timed。 - name: resources description: App 资源库、资源组、礼物配置和我的资源入口。 - name: manager-center @@ -1014,6 +1016,133 @@ paths: $ref: "#/responses/Internal" "502": $ref: "#/responses/UpstreamError" + /api/v1/users/{user_id}/visit: + post: + tags: + - users + summary: 记录一次主页访问并返回目标资料展示状态 + operationId: recordProfileVisit + description: >- + 匿名身份在写入时按访问者当时生效的 anonymous_profile_visit 权益与开关固化;历史记录不会因开关变化反向改名。 + 目标用户当前 hide_profile_data 生效时返回 target_stats_hidden=true,并完全省略 target_stats。 + 当前 App 访问链路无好友或管理员白名单,所有非本人查看者使用同一隐藏结论。 + security: + - BearerAuth: [] + parameters: + - name: user_id + in: path + required: true + type: string + pattern: "^[1-9][0-9]*$" + description: 被访问用户的系统 user_id,不是展示短号。 + responses: + "200": + description: 访问事实已写入或命中同一访客聚合记录。 + schema: + $ref: "#/definitions/ProfileVisitEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + $ref: "#/responses/Forbidden" + "404": + $ref: "#/responses/NotFound" + "409": + description: 极低概率的匿名展示 ID 碰撞,code=CONFLICT;本次事务已回滚,不会降级写入公开访问记录。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "500": + $ref: "#/responses/Internal" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/users/me/visitors: + get: + tags: + - users + summary: 查询访问我的主页的公开与匿名访客 + operationId: listMyProfileVisitors + description: 匿名记录只返回 anonymous_visit_id,省略真实 visitor_user_id;anonymous_visit_id 仅在同一目标用户下稳定,不能用于资料查询。 + security: + - BearerAuth: [] + parameters: + - name: page + in: query + required: false + type: integer + format: int32 + default: 1 + minimum: 1 + maximum: 100 + - name: page_size + in: query + required: false + type: integer + format: int32 + default: 20 + minimum: 1 + maximum: 100 + responses: + "200": + description: 查询成功,公开和匿名记录按最后访问时间统一排序、分别聚合。 + schema: + $ref: "#/definitions/ProfileVisitorListEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + $ref: "#/responses/Forbidden" + "500": + $ref: "#/responses/Internal" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/users/me/appearance: + get: + tags: + - users + - resources + summary: 查询当前用户已佩戴外观资源 + operationId: getMyAppearance + description: >- + 返回 wallet 已过滤过期状态后的当前佩戴快照。mic_seat_animation.format 只来自资源 metadata_json 的显式 + format/animation_format;字段缺省表示格式未配置,客户端不得从素材 URL 后缀推断。 + security: + - BearerAuth: [] + responses: + "200": + description: 查询成功;没有佩戴的单资源槽位返回空对象。 + schema: + $ref: "#/definitions/AppearanceEnvelope" + "401": + $ref: "#/responses/Unauthorized" + "403": + $ref: "#/responses/Forbidden" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/users/by-id/{user_id}/appearance: + get: + tags: + - users + - resources + summary: 查询指定用户的公开已佩戴外观资源 + operationId: getUserAppearance + description: 房间外用户资料展示入口;返回结构与当前用户 appearance 相同,不返回未佩戴或已过期资源。 + parameters: + - name: user_id + in: path + required: true + type: string + pattern: "^[1-9][0-9]*$" + responses: + "200": + description: 查询成功。 + schema: + $ref: "#/definitions/AppearanceEnvelope" + "400": + $ref: "#/responses/BadRequest" + "502": + $ref: "#/responses/UpstreamError" /api/v1/rooms/filters: get: tags: @@ -1223,6 +1352,39 @@ paths: $ref: "#/responses/Conflict" "502": $ref: "#/responses/UpstreamError" + /api/v1/rooms/{room_id}/detail: + get: + tags: + - rooms + summary: 查询房间详情最终快照 + operationId: getRoomDetail + description: 房间首屏、IM 断线重连和版本断层时的权威恢复入口;返回当前背景、房间边框和彩色房名最终态,不依赖历史 IM 是否到达。 + security: + - BearerAuth: [] + parameters: + - name: room_id + in: path + required: true + type: string + pattern: "^[A-Za-z0-9_-]{1,48}$" + maxLength: 48 + responses: + "200": + description: 查询成功;`data.room.active_background/room_border/room_name_style` 缺省表示当前没有对应有效素材。 + schema: + $ref: "#/definitions/RoomDetailEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + $ref: "#/responses/Forbidden" + "404": + $ref: "#/responses/NotFound" + "409": + $ref: "#/responses/Conflict" + "502": + $ref: "#/responses/UpstreamError" /api/v1/rooms/{room_id}/banned-users: get: tags: @@ -1942,6 +2104,476 @@ paths: $ref: "#/responses/Internal" "502": $ref: "#/responses/UpstreamError" + /api/v1/rooms/{room_id}/backgrounds: + get: + tags: + - rooms + summary: 查询房间已保存背景与当前生效背景 + operationId: listRoomBackgrounds + security: + - BearerAuth: [] + parameters: + - name: room_id + in: path + required: true + type: string + responses: + "200": + description: 查询成功;selected_background 返回完整可信媒体快照。 + schema: + $ref: "#/definitions/RoomBackgroundListEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + $ref: "#/responses/Forbidden" + "404": + $ref: "#/responses/NotFound" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/rooms/{room_id}/backgrounds/upload: + post: + tags: + - rooms + summary: 校验权益后上传房间背景媒体 + operationId: uploadRoomBackground + description: 只接受文件头、multipart MIME 和扩展名一致的 JPEG/PNG/GIF/WebP;APNG 不支持。上传不改变房间背景。 + consumes: + - multipart/form-data + security: + - BearerAuth: [] + parameters: + - name: room_id + in: path + required: true + type: string + - name: command_id + in: formData + required: true + type: string + maxLength: 128 + description: 本次“上传并保存背景”的业务幂等键;后续 save 复用同一个值。 + - name: file + in: formData + required: true + type: file + description: 最大 12MiB;宽不超过 2160、高不超过 3840、总像素不超过 8294400;动画最多 120 帧且最长 15 秒。 + responses: + "200": + description: 对象已上传,返回后续保存必须原样提交的 media。 + schema: + $ref: "#/definitions/RoomMediaUploadEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + description: 非房主,或缺少 custom_room_background;后者返回 VIP_BENEFIT_REQUIRED,data.action=upload_room_background。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "413": + description: 文件超过 12MiB,code=FILE_TOO_LARGE。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "415": + description: 文件签名、MIME 或扩展名不一致,code=UNSUPPORTED_MEDIA_TYPE。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "409": + description: 同一 command_id 更换文件,或房间已关闭;更换文件时 code=IDEMPOTENCY_CONFLICT。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "502": + $ref: "#/responses/UpstreamError" + "503": + description: 上传依赖未配置。 + schema: + $ref: "#/definitions/ErrorEnvelope" + /api/v1/rooms/backgrounds/save: + post: + tags: + - rooms + summary: 把专用上传结果保存为房间背景素材 + operationId: saveRoomBackground + description: Fami 必须提交 command_id 与完整 media;Lalu legacy_timed 继续兼容历史 image_url-only 请求。该动作不切换背景、不发 IM。 + security: + - BearerAuth: [] + parameters: + - name: body + in: body + required: true + schema: + $ref: "#/definitions/SaveRoomBackgroundRequest" + responses: + "200": + description: 保存成功或命中同 command_id 的相同素材。 + schema: + $ref: "#/definitions/SaveRoomBackgroundEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + description: 非房主,或 Fami 缺少 custom_room_background;后者返回 data.action=save_room_background。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "404": + $ref: "#/responses/NotFound" + "409": + $ref: "#/responses/Conflict" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/rooms/backgrounds/set: + post: + tags: + - rooms + summary: 在 Room Cell 中切换当前房间背景 + operationId: setRoomBackground + security: + - BearerAuth: [] + parameters: + - name: body + in: body + required: true + schema: + $ref: "#/definitions/SetRoomBackgroundRequest" + responses: + "200": + description: 切换成功或幂等重放;随后异步投递 room_background_changed。 + schema: + $ref: "#/definitions/SetRoomBackgroundEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + description: 非房主,或 Fami 缺少 custom_room_background;后者返回 data.action=set_room_background。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "404": + $ref: "#/responses/NotFound" + "409": + $ref: "#/responses/Conflict" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/rooms/{room_id}/decorations/apply: + post: + tags: + - rooms + summary: 应用房间边框或彩色房名资源 + operationId: applyRoomDecoration + description: 房主专用;wallet 会同时复验当前 VIP benefit、对应 resource_id 和有效 entitlement,再由 Room Cell 固化快照。 + security: + - BearerAuth: [] + parameters: + - name: room_id + in: path + required: true + type: string + - name: body + in: body + required: true + schema: + $ref: "#/definitions/ApplyRoomDecorationRequest" + responses: + "200": + description: 应用成功或幂等重放;随后异步投递 room_decoration_changed。 + schema: + $ref: "#/definitions/ApplyRoomDecorationEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + description: 非房主或对应 VIP 权益未生效;VIP 拒绝包含结构化 data。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "404": + $ref: "#/responses/NotFound" + "409": + $ref: "#/responses/Conflict" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/rooms/{room_id}/messages/images/upload: + post: + tags: + - rooms + summary: 校验发言权限与 VIP 权益后上传房间图片 + operationId: uploadRoomImage + description: 只接受 JPEG/PNG/GIF/WebP;上传前校验房间 active、用户仍在房、未 ban/禁言、公屏开启且 room_image_message 生效。 + consumes: + - multipart/form-data + security: + - BearerAuth: [] + parameters: + - name: room_id + in: path + required: true + type: string + - name: command_id + in: formData + required: true + type: string + maxLength: 128 + description: 本次“上传并发送图片”的业务幂等键;后续 send 复用同一个值。 + - name: file + in: formData + required: true + type: file + description: 最大 8MiB;宽高各不超过 4096、总像素不超过 16777216;动画最多 120 帧且最长 15 秒。 + responses: + "200": + description: 对象已上传,返回发送接口必须原样提交的 media。 + schema: + $ref: "#/definitions/RoomMediaUploadEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + description: 不在房、已 ban/禁言,或缺少 room_image_message;VIP 拒绝包含结构化 data。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "409": + $ref: "#/responses/Conflict" + "413": + description: 文件超过 8MiB,code=FILE_TOO_LARGE。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "415": + description: 文件签名、MIME 或扩展名不一致,code=UNSUPPORTED_MEDIA_TYPE。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "502": + $ref: "#/responses/UpstreamError" + "503": + description: 上传依赖未配置。 + schema: + $ref: "#/definitions/ErrorEnvelope" + /api/v1/rooms/{room_id}/messages/image: + post: + tags: + - rooms + summary: 通过 Room Cell 与 durable outbox 发送房间图片 + operationId: sendRoomImage + security: + - BearerAuth: [] + parameters: + - name: room_id + in: path + required: true + type: string + - name: body + in: body + required: true + schema: + $ref: "#/definitions/SendRoomImageRequest" + responses: + "200": + description: 发送事实已提交;accepted=true 不代表腾讯云 IM 已到达所有客户端。 + schema: + $ref: "#/definitions/SendRoomImageEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + description: 发言权限或 room_image_message 权益未生效;VIP 拒绝包含结构化 data。 + schema: + $ref: "#/definitions/ErrorEnvelope" + "409": + $ref: "#/responses/Conflict" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/vip/me: + get: + tags: + - vip + summary: 查询当前用户的付费、体验卡与最终生效 VIP 状态 + operationId: getMyVIP + description: Flutter 必须以 state.effective_vip 和 state.effective_benefits 授权展示;根层 level/name/active 仅保留旧客户端兼容。 + security: + - BearerAuth: [] + responses: + "200": + description: 查询成功。 + schema: + $ref: "#/definitions/VIPMeEnvelope" + "401": + $ref: "#/responses/Unauthorized" + "403": + $ref: "#/responses/Forbidden" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/vip/packages: + get: + tags: + - vip + summary: 查询当前 App 可购买 VIP 套餐和每级完整权益 + operationId: listVIPPackages + description: >- + benefits[].resource 只有在 resource_id 指向同 App、active 且 resource_type 匹配的真实资源时返回; + 为空时客户端不得根据 resource_id 或权益名拼接素材 URL。套餐目录不代表用户已经拥有权益。 + security: + - BearerAuth: [] + responses: + "200": + description: 查询成功;Fami program_type=tiered_privilege_v1,Lalu 保持 legacy_timed。 + schema: + $ref: "#/definitions/VIPPackageListEnvelope" + "401": + $ref: "#/responses/Unauthorized" + "403": + $ref: "#/responses/Forbidden" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/vip/purchase: + post: + tags: + - vip + summary: 购买、同级续期或升级 VIP + operationId: purchaseVIP + security: + - BearerAuth: [] + parameters: + - name: body + in: body + required: true + schema: + $ref: "#/definitions/VIPPurchaseRequest" + responses: + "200": + description: 购买成功;客户端用 data.state 原子替换 VIP store,并用 coin_balance 替换 COIN 余额。 + schema: + $ref: "#/definitions/VIPPurchaseEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + $ref: "#/responses/Forbidden" + "409": + $ref: "#/responses/Conflict" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/vip/trial-cards/{entitlement_id}/equip: + post: + tags: + - vip + summary: 佩戴一张 VIP 体验卡 + operationId: equipVIPTrialCard + security: + - BearerAuth: [] + parameters: + - name: entitlement_id + in: path + required: true + type: string + description: 背包权益实例 ID;不能用 resource_id 代替。 + responses: + "200": + description: 佩戴成功;客户端使用返回 state,不自行合并付费 VIP。 + schema: + $ref: "#/definitions/VIPTrialCardMutationEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "404": + $ref: "#/responses/NotFound" + "409": + $ref: "#/responses/Conflict" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/vip/trial-cards/equipped: + delete: + tags: + - vip + summary: 卸下当前 VIP 体验卡 + operationId: unequipVIPTrialCard + security: + - BearerAuth: [] + responses: + "200": + description: 卸下成功或当前无卡;卡实例和绝对到期时间不会被删除。 + schema: + $ref: "#/definitions/VIPTrialCardMutationEnvelope" + "401": + $ref: "#/responses/Unauthorized" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/vip/settings: + get: + tags: + - vip + summary: 查询当前 App 的 VIP 功能开关 + operationId: getVIPSettings + security: + - BearerAuth: [] + responses: + "200": + description: 查询成功;开关是偏好,不单独授予权益。 + schema: + $ref: "#/definitions/VIPSettingsEnvelope" + "401": + $ref: "#/responses/Unauthorized" + "502": + $ref: "#/responses/UpstreamError" + patch: + tags: + - vip + summary: 局部更新当前 App 的 VIP 功能开关 + operationId: patchVIPSettings + security: + - BearerAuth: [] + parameters: + - name: body + in: body + required: true + schema: + $ref: "#/definitions/VIPSettingsPatchRequest" + responses: + "200": + description: 更新成功;显式 false 与字段未提交语义不同。 + schema: + $ref: "#/definitions/VIPSettingsEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "502": + $ref: "#/responses/UpstreamError" + /api/v1/vip/online-notice: + post: + tags: + - vip + summary: 请求服务端创建 VIP 上线全服通知 + operationId: triggerVIPOnlineNotice + description: 服务端执行前重新校验 online_global_notice 权益和用户开关;Flutter 不能自行向全局群发消息。 + security: + - BearerAuth: [] + parameters: + - name: body + in: body + required: true + schema: + $ref: "#/definitions/CommandIDRequest" + responses: + "200": + description: 已创建或命中相同 command_id 的异步播报事实。 + schema: + $ref: "#/definitions/VIPOnlineNoticeEnvelope" + "400": + $ref: "#/responses/BadRequest" + "401": + $ref: "#/responses/Unauthorized" + "403": + description: 权益或用户开关未生效;返回 VIP_BENEFIT_REQUIRED 结构化 data。 + schema: + $ref: "#/definitions/VIPBenefitRequiredErrorEnvelope" + "502": + $ref: "#/responses/UpstreamError" /api/v1/resources: get: tags: @@ -1953,7 +2585,7 @@ paths: in: query required: false type: string - enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, emoji_pack] + enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, room_border, room_name_style, emoji_pack, vip_trial_card] - name: keyword in: query required: false @@ -1997,7 +2629,7 @@ paths: in: query required: false type: string - enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, emoji_pack] + enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, room_border, room_name_style, emoji_pack, vip_trial_card] - name: page in: query required: false @@ -2181,7 +2813,7 @@ paths: in: query required: false type: string - enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, emoji_pack] + enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, room_border, room_name_style, emoji_pack, vip_trial_card] responses: "200": description: 返回当前用户有效资源权益。 @@ -2200,7 +2832,9 @@ paths: tags: - resources summary: 佩戴我的资源 - description: vip_trial_card 会在 wallet-service 内部委托 VIP 专用状态机;该通用响应不含完整 VipState,VIP 客户端应使用 /api/v1/vip/trial-cards/{entitlement_id}/equip。 + description: >- + Fami 请求必须带 command_id,同一动作重试复用;wallet 在写入前重新校验当前 VIP benefit 与 resource_id 的绑定。 + vip_trial_card 会委托 VIP 专用状态机;该通用响应不含完整 VipState,VIP 客户端应使用 /api/v1/vip/trial-cards/{entitlement_id}/equip。 operationId: equipMyResource security: - BearerAuth: [] @@ -2221,9 +2855,15 @@ paths: "401": $ref: "#/responses/Unauthorized" "403": - $ref: "#/responses/Forbidden" + description: VIP 绑定资源的对应权益已失效时返回 VIP_BENEFIT_REQUIRED,并包含 benefit_code/required_level/current_effective_level/action。 + schema: + $ref: "#/definitions/ErrorEnvelope" "404": $ref: "#/responses/NotFound" + "409": + description: 同一 command_id 改变 resource_id 或 entitlement_id,code=IDEMPOTENCY_CONFLICT。 + schema: + $ref: "#/definitions/ErrorEnvelope" "502": $ref: "#/responses/UpstreamError" /api/v1/messages/tabs: @@ -2525,25 +3165,32 @@ definitions: - PERMISSION_DENIED - NOT_FOUND - CONFLICT + - ROOM_CLOSED - INSUFFICIENT_BALANCE + - IDEMPOTENCY_CONFLICT + - FILE_TOO_LARGE + - UNSUPPORTED_MEDIA_TYPE + - INVALID_MEDIA_DIMENSIONS + - VIP_BENEFIT_REQUIRED + - VIP_PROGRAM_INACTIVE + - VIP_PACKAGE_NOT_PURCHASABLE + - VIP_DOWNGRADE_NOT_ALLOWED + - VIP_RECHARGE_REQUIRED + - VIP_INSUFFICIENT_COIN + - VIP_TRIAL_CARD_NOT_FOUND + - VIP_TRIAL_CARD_EXPIRED + - VIP_COIN_REBATE_NOT_FOUND + - VIP_COIN_REBATE_EXPIRED message: type: string - enum: - - invalid request body - - unauthorized - - upstream service error - - internal error - - invalid argument - - rate limited - - authentication failed - - conflict - - insufficient balance - - permission denied - - profile required - - not found + description: 稳定的短错误说明,仅用于兜底展示;客户端业务分支必须使用 code。 request_id: type: string - description: gateway 业务 API 失败响应外壳;失败响应不返回 `data`。 + data: + type: object + additionalProperties: {} + description: 可选的安全结构化业务详情;当前 `VIP_BENEFIT_REQUIRED` 返回 benefit_code、required_level、current_effective_level 和 action。不得包含内部错误。 + description: gateway 业务 API 失败响应外壳;多数失败省略 `data`,需要客户端执行动作的错误可返回安全结构化详情。 HealthResponse: type: object properties: @@ -3512,6 +4159,8 @@ definitions: format: uint64 cover_url: type: string + background_url: + type: string description: 开屏图片 URL。 type: type: string @@ -3782,7 +4431,7 @@ definitions: type: string resource_type: type: string - enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, emoji_pack] + enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, room_border, room_name_style, emoji_pack, vip_trial_card] name: type: string status: @@ -3796,6 +4445,14 @@ definitions: wallet_asset_amount: type: integer format: int64 + price_type: + type: string + coin_price: + type: integer + format: int64 + gift_point_amount: + type: integer + format: int64 usage_scopes: type: array items: @@ -3806,11 +4463,114 @@ definitions: type: string animation_url: type: string + format: + type: string + description: 可选的客户端渲染格式。由 metadata_json.format 读取,缺省时兼容读取 metadata_json.animation_format,并统一转为小写;两个字段都没有时省略。mic_seat_animation 与 room_border 后台创建值限于 png/jpeg/webp/gif/svga/pag/mp4;其他资源类型可以返回自身样式标识。客户端不得根据 asset_url、preview_url 或 animation_url 的 URL 后缀推断格式。 metadata_json: type: string sort_order: type: integer format: int32 + created_at_ms: + type: integer + format: int64 + updated_at_ms: + type: integer + format: int64 + AppearanceResourceData: + type: object + description: >- + wallet 当前佩戴资源的客户端展示快照;单资源槽位没有佩戴时返回空对象。format 只来自 metadata_json + 的显式 format/animation_format,字段缺省时客户端不得从任一 URL 后缀推断。 + properties: + resource_id: + type: integer + format: int64 + resource_code: + type: string + resource_type: + type: string + enum: [avatar_frame, profile_card, vehicle, badge, mic_seat_animation] + name: + type: string + asset_url: + type: string + preview_url: + type: string + animation_url: + type: string + format: + type: string + description: mic_seat_animation 当前只允许 png/jpeg/webp/gif/svga/pag/mp4;其它外观类型保留自身显式格式值。 + metadata_json: + type: string + entitlement_id: + type: string + expires_at_ms: + type: integer + format: int64 + badge_form: + type: string + badge_kind: + type: string + level_track: + type: string + AppearanceData: + type: object + required: + - user_id + - avatar_frame + - profile_card + - vehicle + - mic_seat_animation + - level + - level_badges + - equipped_badges + - badges + - badge_profile + - resources + - server_time_ms + properties: + user_id: + type: string + avatar_frame: + $ref: "#/definitions/AppearanceResourceData" + profile_card: + $ref: "#/definitions/AppearanceResourceData" + vehicle: + $ref: "#/definitions/AppearanceResourceData" + mic_seat_animation: + $ref: "#/definitions/AppearanceResourceData" + level: + type: object + level_badges: + type: object + equipped_badges: + type: array + items: + $ref: "#/definitions/AppearanceResourceData" + badges: + type: array + items: + type: object + badge_profile: + type: object + resources: + type: array + items: + $ref: "#/definitions/AppearanceResourceData" + server_time_ms: + type: integer + format: int64 + AppearanceEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + required: + - data + properties: + data: + $ref: "#/definitions/AppearanceData" ResourceListData: type: object properties: @@ -3835,7 +4595,7 @@ definitions: description: 资源 ID;按字符串返回避免 64 位整数在客户端丢精度。 resource_type: type: string - enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, emoji_pack] + enum: [avatar_frame, profile_card, coin, vehicle, chat_bubble, badge, floating_screen, gift, mic_seat_icon, mic_seat_animation, room_border, room_name_style, emoji_pack, vip_trial_card] name: type: string asset_url: @@ -4149,6 +4909,12 @@ definitions: format: int64 source_grant_id: type: string + created_at_ms: + type: integer + format: int64 + updated_at_ms: + type: integer + format: int64 equipped: type: boolean description: 当前权益是否是该资源类型的已佩戴项。 @@ -4164,10 +4930,16 @@ definitions: format: int32 EquipUserResourceRequest: type: object + required: + - command_id properties: entitlement_id: type: string description: 可选;不传时服务端自动选择当前用户该资源最新有效权益。 + command_id: + type: string + maxLength: 128 + description: Fami 必填的业务幂等键;同一佩戴动作重试必须复用。Lalu 旧客户端未传时仅保留单请求兼容,不承诺跨请求重放。 RechargeProductData: type: object properties: @@ -4764,6 +5536,15 @@ definitions: version: type: integer format: int64 + online_count: + type: integer + format: int32 + room_border: + $ref: "#/definitions/RoomDecorationData" + room_name_style: + $ref: "#/definitions/RoomDecorationData" + active_background: + $ref: "#/definitions/RoomBackgroundData" RoomBanState: type: object properties: @@ -5053,6 +5834,8 @@ definitions: type: string cover_url: type: string + background_url: + type: string description: type: string owner_user_id: @@ -5078,9 +5861,18 @@ definitions: occupied_seat_count: type: integer format: int32 + visible_region_id: + type: integer + format: int64 version: type: integer format: int64 + active_background: + $ref: "#/definitions/RoomBackgroundData" + room_border: + $ref: "#/definitions/RoomDecorationData" + room_name_style: + $ref: "#/definitions/RoomDecorationData" RoomViewerData: type: object properties: @@ -5111,6 +5903,16 @@ definitions: publish_deadline_ms: type: integer format: int64 + mic_muted: + type: boolean + seat_status: + type: string + mic_heartbeat_at_ms: + type: integer + format: int64 + gift_value: + type: integer + format: int64 RoomRankItemData: type: object properties: @@ -5151,10 +5953,49 @@ definitions: type: string display_user_id: type: string + pretty_id: + type: string + pretty_display_user_id: + type: string username: type: string avatar: type: string + gender: + type: string + age: + type: integer + format: int32 + country: + type: string + country_name: + type: string + country_display_name: + type: string + country_flag: + type: string + vip: + type: object + level: + type: object + level_badges: + type: object + badges: + type: array + items: + type: object + avatar_frame: + $ref: "#/definitions/AppearanceResourceData" + profile_card: + $ref: "#/definitions/AppearanceResourceData" + vehicle: + $ref: "#/definitions/AppearanceResourceData" + mic_seat_animation: + $ref: "#/definitions/AppearanceResourceData" + description: 当前麦位动效佩戴快照;空对象表示未佩戴,format 缺省表示后台没有配置受支持 renderer。 + charm: + type: integer + format: int64 RoomIMData: type: object properties: @@ -5234,6 +6075,75 @@ definitions: server_time_ms: type: integer format: int64 + RoomPermissionsData: + type: object + properties: + is_owner: + type: boolean + is_admin: + type: boolean + is_muted: + type: boolean + can_speak: + type: boolean + can_send_gift: + type: boolean + can_mic_up: + type: boolean + can_manage_room: + type: boolean + can_leave_room: + type: boolean + can_close_room: + type: boolean + can_mute_mic: + type: boolean + can_open_gift_box: + type: boolean + RoomDetailData: + type: object + required: + - room + - viewer + - is_followed + - seats + - rank_top + - online_count + - permissions + - im + - rtc + - server_time_ms + properties: + room: + $ref: "#/definitions/RoomInitialData" + viewer: + $ref: "#/definitions/RoomViewerData" + is_followed: + type: boolean + seats: + type: array + items: + $ref: "#/definitions/RoomSeatData" + rank_top: + type: array + items: + $ref: "#/definitions/RoomRankItemData" + online_count: + type: integer + format: int32 + permissions: + $ref: "#/definitions/RoomPermissionsData" + profiles: + type: array + items: + $ref: "#/definitions/RoomUserProfileData" + im: + $ref: "#/definitions/RoomIMData" + rtc: + $ref: "#/definitions/RoomRTCData" + server_time_ms: + type: integer + format: int64 RoomContributionRankData: type: object required: @@ -5791,6 +6701,13 @@ definitions: properties: data: $ref: "#/definitions/RoomSnapshotData" + RoomDetailEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/RoomDetailData" RoomContributionRankEnvelope: allOf: - $ref: "#/definitions/GatewayOKEnvelopeBase" @@ -5938,3 +6855,999 @@ definitions: properties: data: $ref: "#/definitions/CoinSellerTransferResponse" + ProfileVisitData: + type: object + required: + - recorded + - target_stats_hidden + properties: + recorded: + type: boolean + target_stats_hidden: + type: boolean + description: true 时 target_stats 字段完全省略;该值按目标用户当前 VIP 权益实时判定。 + target_stats: + $ref: "#/definitions/MyOverviewProfileStatsData" + target_profile: + $ref: "#/definitions/UserProfileData" + ProfileVisitEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/ProfileVisitData" + ProfileVisitorRecordData: + type: object + required: + - visitor_identity_hidden + - target_user_id + - visit_count + - last_visited_at_ms + properties: + visitor_user_id: + type: string + description: 只在 visitor_identity_hidden=false 时返回。 + anonymous_visit_id: + type: string + description: 只在 visitor_identity_hidden=true 时返回;随机且仅在同一目标用户下稳定,不能查询资料。 + visitor_identity_hidden: + type: boolean + target_user_id: + type: string + visit_count: + type: integer + format: int64 + last_visited_at_ms: + type: integer + format: int64 + ProfileVisitorListData: + type: object + required: + - records + - total + properties: + records: + type: array + items: + $ref: "#/definitions/ProfileVisitorRecordData" + total: + type: integer + format: int64 + ProfileVisitorListEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/ProfileVisitorListData" + VIPStatusData: + type: object + required: + - level + - name + - active + - started_at_ms + - expires_at_ms + properties: + level: + type: integer + format: int32 + name: + type: string + active: + type: boolean + started_at_ms: + type: integer + format: int64 + expires_at_ms: + type: integer + format: int64 + program_type: + type: string + enum: [tiered_privilege_v1, legacy_timed] + config_version: + type: integer + format: int64 + VIPProgramConfigData: + type: object + properties: + app_code: + type: string + program_type: + type: string + enum: [tiered_privilege_v1, legacy_timed] + level_count: + type: integer + format: int32 + same_level_expiry_policy: + type: string + upgrade_expiry_policy: + type: string + downgrade_purchase_policy: + type: string + benefit_inheritance_policy: + type: string + grant_mode: + type: string + trial_card_enabled: + type: boolean + status: + type: string + config_version: + type: integer + format: int64 + updated_by_admin_id: + type: integer + format: int64 + created_at_ms: + type: integer + format: int64 + updated_at_ms: + type: integer + format: int64 + VIPBenefitPreviewItemData: + type: object + properties: + preview_id: + type: string + title: + type: string + media_type: + type: string + asset_url: + type: string + preview_url: + type: string + animation_url: + type: string + composition_type: + type: string + sort_order: + type: integer + format: int32 + VIPBenefitNumericRewardData: + type: object + properties: + label: + type: string + value: + type: integer + format: int64 + unit: + type: string + period: + type: string + VIPBenefitPresentationData: + type: object + required: + - description + - preview_items + - numeric_reward + properties: + description: + type: string + preview_items: + type: array + items: + $ref: "#/definitions/VIPBenefitPreviewItemData" + numeric_reward: + $ref: "#/definitions/VIPBenefitNumericRewardData" + x-nullable: true + description: 非数值奖励权益返回 null。 + VIPBenefitData: + type: object + required: + - benefit_code + - name + - benefit_type + - unlock_level + - status + - trial_enabled + - resource_id + - resource_type + - execution_scope + - auto_equip + - sort_order + - metadata_json + - presentation + properties: + benefit_code: + type: string + name: + type: string + benefit_type: + type: string + unlock_level: + type: integer + format: int32 + status: + type: string + trial_enabled: + type: boolean + resource_id: + type: integer + format: int64 + resource_type: + type: string + execution_scope: + type: string + auto_equip: + type: boolean + sort_order: + type: integer + format: int32 + metadata_json: + type: string + created_at_ms: + type: integer + format: int64 + updated_at_ms: + type: integer + format: int64 + presentation: + $ref: "#/definitions/VIPBenefitPresentationData" + x-nullable: true + description: metadata_json 没有结构化 presentation 时固定返回 null。 + resource: + $ref: "#/definitions/ResourceData" + description: 仅在 wallet 已确认资源真实、同 App、active 且类型匹配时返回;否则字段省略。真实资源缺失时 benefit 必须保持 disabled 且 resource_id=0,后台应先建 resource 再绑定。 + VIPRewardItemData: + type: object + properties: + resource_id: + type: integer + format: int64 + resource_code: + type: string + resource_type: + type: string + name: + type: string + quantity: + type: integer + format: int64 + expires_at_ms: + type: integer + format: int64 + cover_url: + type: string + asset_url: + type: string + preview_url: + type: string + animation_url: + type: string + VIPPackageData: + type: object + properties: + level: + type: integer + format: int32 + name: + type: string + status: + type: string + price_coin: + type: integer + format: int64 + duration_ms: + type: integer + format: int64 + reward_resource_group_id: + type: integer + format: int64 + reward_items: + type: array + items: + $ref: "#/definitions/VIPRewardItemData" + can_purchase: + type: boolean + sort_order: + type: integer + format: int32 + recharge_gate_required: + type: boolean + required_recharge_coin_amount: + type: integer + format: int64 + user_recharge_coin_amount: + type: integer + format: int64 + purchase_locked_reason: + type: string + benefits: + type: array + items: + $ref: "#/definitions/VIPBenefitData" + config_version: + type: integer + format: int64 + VIPTrialCardData: + type: object + properties: + trial_card_id: + type: string + entitlement_id: + type: string + resource_id: + type: integer + format: int64 + user_id: + type: integer + format: int64 + level: + type: integer + format: int32 + name: + type: string + status: + type: string + equipped: + type: boolean + duration_ms: + type: integer + format: int64 + effective_at_ms: + type: integer + format: int64 + expires_at_ms: + type: integer + format: int64 + remaining_duration_ms: + type: integer + format: int64 + grant_source: + type: string + source_grant_id: + type: string + created_at_ms: + type: integer + format: int64 + updated_at_ms: + type: integer + format: int64 + VIPUserSettingsData: + type: object + required: + - app_code + - user_id + - room_entry_notice_enabled + - online_global_notice_enabled + - hide_profile_data_enabled + - anonymous_profile_visit_enabled + - leaderboard_invisible_enabled + - updated_at_ms + properties: + app_code: + type: string + user_id: + type: integer + format: int64 + room_entry_notice_enabled: + type: boolean + online_global_notice_enabled: + type: boolean + hide_profile_data_enabled: + type: boolean + anonymous_profile_visit_enabled: + type: boolean + leaderboard_invisible_enabled: + type: boolean + updated_at_ms: + type: integer + format: int64 + VIPStateData: + type: object + required: + - paid_vip + - equipped_trial_card + - effective_vip + - effective_source + - effective_benefits + - evaluated_at_ms + - program_config + - user_settings + properties: + paid_vip: + $ref: "#/definitions/VIPStatusData" + equipped_trial_card: + $ref: "#/definitions/VIPTrialCardData" + x-nullable: true + description: 当前没有佩戴体验卡时固定返回 null。 + effective_vip: + $ref: "#/definitions/VIPStatusData" + effective_source: + type: string + effective_benefits: + type: array + items: + $ref: "#/definitions/VIPBenefitData" + evaluated_at_ms: + type: integer + format: int64 + program_config: + $ref: "#/definitions/VIPProgramConfigData" + user_settings: + $ref: "#/definitions/VIPUserSettingsData" + VIPMeData: + allOf: + - $ref: "#/definitions/VIPStatusData" + - type: object + required: + - program_config + - state + properties: + program_config: + $ref: "#/definitions/VIPProgramConfigData" + state: + $ref: "#/definitions/VIPStateData" + VIPMeEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/VIPMeData" + VIPPackageListData: + type: object + required: + - current_vip + - packages + - program_config + - state + properties: + current_vip: + $ref: "#/definitions/VIPStatusData" + packages: + type: array + items: + $ref: "#/definitions/VIPPackageData" + program_config: + $ref: "#/definitions/VIPProgramConfigData" + state: + $ref: "#/definitions/VIPStateData" + VIPPackageListEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/VIPPackageListData" + CommandIDRequest: + type: object + required: + - command_id + properties: + command_id: + type: string + minLength: 1 + maxLength: 128 + description: 业务幂等键;同一用户动作的超时或 5xx 重试必须复用。 + VIPPurchaseRequest: + allOf: + - $ref: "#/definitions/CommandIDRequest" + - type: object + required: + - level + properties: + level: + type: integer + format: int32 + minimum: 1 + VIPAssetBalanceData: + type: object + properties: + asset_type: + type: string + available_amount: + type: integer + format: int64 + frozen_amount: + type: integer + format: int64 + version: + type: integer + format: int64 + VIPPurchaseData: + type: object + properties: + order_id: + type: string + transaction_id: + type: string + vip: + $ref: "#/definitions/VIPStatusData" + coin_spent: + type: integer + format: int64 + coin_balance_after: + type: integer + format: int64 + coin_balance: + $ref: "#/definitions/VIPAssetBalanceData" + reward_items: + type: array + items: + $ref: "#/definitions/VIPRewardItemData" + program_config: + $ref: "#/definitions/VIPProgramConfigData" + state: + $ref: "#/definitions/VIPStateData" + VIPPurchaseEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/VIPPurchaseData" + VIPTrialCardMutationData: + type: object + properties: + trial_card: + $ref: "#/definitions/VIPTrialCardData" + unequipped: + type: boolean + program_config: + $ref: "#/definitions/VIPProgramConfigData" + state: + $ref: "#/definitions/VIPStateData" + server_time_ms: + type: integer + format: int64 + VIPTrialCardMutationEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/VIPTrialCardMutationData" + VIPSettingsPatchRequest: + type: object + minProperties: 1 + properties: + room_entry_notice_enabled: + type: boolean + online_global_notice_enabled: + type: boolean + hide_profile_data_enabled: + type: boolean + anonymous_profile_visit_enabled: + type: boolean + leaderboard_invisible_enabled: + type: boolean + VIPSettingsData: + type: object + required: + - settings + properties: + settings: + $ref: "#/definitions/VIPUserSettingsData" + evaluated_at_ms: + type: integer + format: int64 + server_time_ms: + type: integer + format: int64 + VIPSettingsEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/VIPSettingsData" + VIPOnlineNoticeData: + type: object + required: + - event_id + - group_id + - status + - created + - evaluated_at_ms + properties: + event_id: + type: string + group_id: + type: string + status: + type: string + enum: [pending] + created: + type: boolean + evaluated_at_ms: + type: integer + format: int64 + VIPOnlineNoticeEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/VIPOnlineNoticeData" + VIPBenefitRequiredData: + type: object + required: + - benefit_code + - required_level + - current_effective_level + - action + properties: + benefit_code: + type: string + required_level: + type: integer + format: int32 + description: 当前 App 首个显式配置该权益的 active 等级;0 表示未配置。 + current_effective_level: + type: integer + format: int32 + action: + type: string + description: 稳定的被拒绝动作标识;背景链路分别为 upload_room_background、save_room_background、set_room_background。 + VIPBenefitRequiredErrorEnvelope: + type: object + required: + - code + - message + - request_id + - data + properties: + code: + type: string + enum: [VIP_BENEFIT_REQUIRED] + message: + type: string + enum: [vip benefit is required] + request_id: + type: string + data: + $ref: "#/definitions/VIPBenefitRequiredData" + RoomMediaData: + type: object + required: + - url + - object_key + - content_type + - format + - size_bytes + - width + - height + - animated + - frame_count + - duration_ms + - sha256 + - status + properties: + url: + type: string + description: 专用上传入口返回的 HTTPS 对象地址;提交 save/send 时必须原样回传。 + object_key: + type: string + description: 专用上传入口返回的对象 key,绑定 App、用户、房间和用途。 + content_type: + type: string + enum: [image/jpeg, image/png, image/gif, image/webp] + format: + type: string + enum: [jpeg, png, gif, webp] + size_bytes: + type: integer + format: int64 + width: + type: integer + format: int32 + height: + type: integer + format: int32 + animated: + type: boolean + frame_count: + type: integer + format: int32 + duration_ms: + type: integer + format: int64 + description: 静态图固定为 0。 + poster_url: + type: string + description: 当前上传链路不生成 poster,HTTP 空值时省略。 + thumbnail_url: + type: string + description: 当前上传链路不生成 thumbnail,HTTP 空值时省略。 + sha256: + type: string + pattern: "^[0-9a-f]{64}$" + status: + type: string + enum: [active] + RoomMediaUploadData: + type: object + required: + - purpose + - room_id + - command_id + - room_version + - media + - server_time_ms + properties: + purpose: + type: string + enum: [room_background, room_image] + room_id: + type: string + command_id: + type: string + room_version: + type: integer + format: int64 + description: 上传授权时观察到的房间版本,不是后续 send/set 的成功版本。 + media: + $ref: "#/definitions/RoomMediaData" + server_time_ms: + type: integer + format: int64 + RoomMediaUploadEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/RoomMediaUploadData" + RoomBackgroundData: + type: object + required: + - background_id + - room_id + - image_url + - created_at_ms + - updated_at_ms + properties: + background_id: + type: integer + format: int64 + room_id: + type: string + image_url: + type: string + created_by_user_id: + type: string + created_at_ms: + type: integer + format: int64 + updated_at_ms: + type: integer + format: int64 + command_id: + type: string + media: + $ref: "#/definitions/RoomMediaData" + SaveRoomBackgroundRequest: + type: object + required: + - room_id + properties: + room_id: + type: string + command_id: + type: string + maxLength: 128 + description: Fami 必填;与专用上传复用。Lalu image_url-only 兼容请求可省略。 + image_url: + type: string + description: Fami 可省略并由 media.url 补齐;Lalu 旧协议必填。 + media: + $ref: "#/definitions/RoomMediaData" + description: Fami 必填,必须原样使用专用背景上传返回对象。 + SaveRoomBackgroundData: + type: object + properties: + background: + $ref: "#/definitions/RoomBackgroundData" + server_time_ms: + type: integer + format: int64 + SaveRoomBackgroundEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/SaveRoomBackgroundData" + RoomBackgroundListData: + type: object + required: + - backgrounds + - server_time_ms + properties: + backgrounds: + type: array + items: + $ref: "#/definitions/RoomBackgroundData" + selected_background_url: + type: string + selected_background: + $ref: "#/definitions/RoomBackgroundData" + server_time_ms: + type: integer + format: int64 + RoomBackgroundListEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/RoomBackgroundListData" + SetRoomBackgroundRequest: + type: object + required: + - room_id + - command_id + - background_id + properties: + room_id: + type: string + command_id: + type: string + maxLength: 128 + background_id: + type: string + pattern: "^[1-9][0-9]*$" + description: 支持 JSON 数字或数字字符串;这里按字符串建模避免 64 位精度丢失。 + RoomDecorationData: + type: object + required: + - resource_id + - resource_code + - resource_type + - name + - asset_url + - entitlement_id + properties: + resource_id: + type: integer + format: int64 + resource_code: + type: string + resource_type: + type: string + enum: [room_border, room_name_style] + name: + type: string + asset_url: + type: string + preview_url: + type: string + animation_url: + type: string + format: + type: string + metadata_json: + type: string + entitlement_id: + type: string + text_colors: + type: array + maxItems: 8 + items: + type: string + pattern: "^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$" + stops: + type: array + items: + type: number + format: double + angle_degrees: + type: number + format: double + expires_at_ms: + type: integer + format: int64 + SetRoomBackgroundData: + type: object + properties: + result: + $ref: "#/definitions/CommandResult" + room: + $ref: "#/definitions/RoomInitialData" + background: + $ref: "#/definitions/RoomBackgroundData" + server_time_ms: + type: integer + format: int64 + SetRoomBackgroundEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/SetRoomBackgroundData" + ApplyRoomDecorationRequest: + type: object + required: + - command_id + - decoration_type + - resource_id + properties: + command_id: + type: string + maxLength: 128 + decoration_type: + type: string + enum: [room_border, colored_room_name] + description: colored_room_name 对应资源目录类型 room_name_style。 + resource_id: + type: string + pattern: "^[1-9][0-9]*$" + description: 支持 JSON 数字或数字字符串。 + entitlement_id: + type: string + description: 可选;建议传背包实例 ID,省略时服务端选择一个当前有效实例。 + ApplyRoomDecorationData: + type: object + properties: + result: + $ref: "#/definitions/CommandResult" + room: + $ref: "#/definitions/RoomInitialData" + decoration_type: + type: string + enum: [room_border, colored_room_name] + resource: + $ref: "#/definitions/RoomDecorationData" + server_time_ms: + type: integer + format: int64 + ApplyRoomDecorationEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/ApplyRoomDecorationData" + SendRoomImageRequest: + type: object + required: + - command_id + - image + properties: + command_id: + type: string + maxLength: 128 + description: 与专用图片上传复用;同一动作重试不得更换。 + image: + $ref: "#/definitions/RoomMediaData" + description: 必须原样使用专用图片上传返回的 media。 + SendRoomImageData: + type: object + required: + - result + - accepted + - message_id + - room_id + - sender_user_id + - server_time_ms + - image + properties: + result: + $ref: "#/definitions/CommandResult" + accepted: + type: boolean + message_id: + type: string + description: 由 app_code、room_id 和 command_id 稳定生成;IM 去重优先使用 event_id,消息展示去重也可使用 message_id。 + room_id: + type: string + sender_user_id: + type: string + server_time_ms: + type: integer + format: int64 + image: + $ref: "#/definitions/RoomMediaData" + SendRoomImageEnvelope: + allOf: + - $ref: "#/definitions/GatewayOKEnvelopeBase" + - type: object + properties: + data: + $ref: "#/definitions/SendRoomImageData" diff --git a/docs/接口清单.md b/docs/接口清单.md index 06a88141..72ae2709 100644 --- a/docs/接口清单.md +++ b/docs/接口清单.md @@ -35,6 +35,7 @@ | GET | `/api/v1/users/by-display-user-id/{display_user_id}` | users | `resolveDisplayUserID` | 通过当前展示短号解析用户 | | GET | `/api/v1/users/profiles:batch` | users | `batchUserProfiles` | 批量查询会话展示资料 | | GET | `/api/v1/users/room-display-profiles:batch` | users | `batchRoomDisplayProfiles` | 批量查询房间和公屏展示资料 | +| GET | `/api/v1/users/voice-room-presence:batch` | users | `batchUserVoiceRoomPresences` | 批量查询个人信息页可展示的用户上麦入口 | | GET | `/api/v1/users/me/identity` | users | `getMyIdentity` | 查询当前用户展示短号状态 | | GET | `/api/v1/users/me/host-identity` | users | `getMyHostIdentity` | 查询当前用户 Host/Agency/BD 身份 | | GET | `/api/v1/users/me/overview` | users | `getMyOverview` | 我的页首屏聚合摘要 | diff --git a/pkg/tencentcos/uploader.go b/pkg/tencentcos/uploader.go index afc90019..4b4da094 100644 --- a/pkg/tencentcos/uploader.go +++ b/pkg/tencentcos/uploader.go @@ -86,3 +86,26 @@ func (u *Uploader) PutObject(ctx context.Context, key string, reader io.Reader, return publicURL, nil } + +// OwnsObjectURL 证明 URL 正好属于当前配置的 access_url 且对应同一个 object key。 +// gateway 在接受客户端回传媒体元数据时必须调用它,不能只相信 https 或 URL 后缀。 +func (u *Uploader) OwnsObjectURL(objectURL string, objectKey string) bool { + if u == nil { + return false + } + objectKey = strings.TrimLeft(strings.TrimSpace(objectKey), "/") + if objectKey == "" { + return false + } + expected, err := url.JoinPath(strings.TrimRight(u.accessURL, "/"), objectKey) + if err != nil { + return false + } + actualURL, actualErr := url.Parse(strings.TrimSpace(objectURL)) + expectedURL, expectedErr := url.Parse(expected) + if actualErr != nil || expectedErr != nil || actualURL.Scheme != "https" || actualURL.RawQuery != "" || actualURL.Fragment != "" { + return false + } + return strings.EqualFold(actualURL.Scheme, expectedURL.Scheme) && strings.EqualFold(actualURL.Host, expectedURL.Host) && + actualURL.EscapedPath() == expectedURL.EscapedPath() +} diff --git a/pkg/tencentim/rest_client.go b/pkg/tencentim/rest_client.go index 6710f796..e5d93027 100644 --- a/pkg/tencentim/rest_client.go +++ b/pkg/tencentim/rest_client.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/rand" + "crypto/sha256" "encoding/binary" "encoding/json" "errors" @@ -287,8 +288,11 @@ func (c *RESTClient) PublishGroupCustomMessage(ctx context.Context, message Cust } request := sendGroupMsgRequest{ - GroupID: message.GroupID, - Random: randomUint32(), + GroupID: message.GroupID, + // 腾讯云群消息会在短窗口内按 Random + 完整消息内容去重。outbox 的 event_id + // 对同一业务事实稳定,因此从它派生 Random 可收敛“腾讯已成功、本地尚未标记”后的补偿重试; + // 客户端仍应按 payload.event_id 去重,以覆盖超出腾讯去重窗口后的极端重放。 + Random: stableMessageRandom(message.EventID), CloudCustomData: string(payload), FromAccount: message.FromAccount, MsgBody: []messageElement{ @@ -312,6 +316,11 @@ func (c *RESTClient) PublishGroupCustomMessage(ctx context.Context, message Cust return response.err() } +func stableMessageRandom(eventID string) uint32 { + sum := sha256.Sum256([]byte(strings.TrimSpace(eventID))) + return binary.BigEndian.Uint32(sum[:4]) +} + // PublishUserCustomMessage 向单个腾讯云 IM identifier 发送 TIMCustomElem。 // 该方法只负责投递协议;业务幂等、隐私字段和重试策略必须由调用方在 outbox 中保证。 func (c *RESTClient) PublishUserCustomMessage(ctx context.Context, message CustomUserMessage) error { diff --git a/pkg/walletmq/messages.go b/pkg/walletmq/messages.go index 8370637b..fbb2dacb 100644 --- a/pkg/walletmq/messages.go +++ b/pkg/walletmq/messages.go @@ -35,6 +35,7 @@ const ( EventTypeWalletRedPacketClaimed = "WalletRedPacketClaimed" EventTypeWalletRedPacketRefunded = "WalletRedPacketRefunded" EventTypeVIPActivated = "VipActivated" + EventTypeVIPProgramChanged = "VipProgramChanged" EventTypeVIPDailyCoinRebateAvailable = "VipDailyCoinRebateAvailable" EventTypeResourceGranted = "ResourceGranted" EventTypeResourceGroupGranted = "ResourceGroupGranted" diff --git a/pkg/xerr/errors.go b/pkg/xerr/errors.go index ca386c2a..176e30cb 100644 --- a/pkg/xerr/errors.go +++ b/pkg/xerr/errors.go @@ -3,6 +3,7 @@ package xerr import ( "errors" "fmt" + "strings" ) // Code 是服务内部稳定错误原因。 @@ -174,6 +175,10 @@ const ( type Error struct { Code Code Message string + // Metadata 只保存业务层显式声明可跨服务、可返回客户端的短字段。 + // 原始错误、SQL、配置和第三方响应不得写入这里;ToGRPCError 会把它投影到 + // google.rpc.ErrorInfo.metadata,gateway 再原样写入 envelope.data。 + Metadata map[string]string } // Error 实现标准 error 接口。 @@ -195,6 +200,17 @@ func New(code Code, message string) error { } } +// NewWithMetadata 构造带安全结构化详情的业务错误。 +// metadata 是显式的 wire contract,不接受空键;键和值都会去除首尾空白并复制, +// 防止调用方后续修改 map 导致同一个错误在日志与 wire 上出现不同语义。 +func NewWithMetadata(code Code, message string, metadata map[string]string) error { + return &Error{ + Code: code, + Message: message, + Metadata: normalizeMetadata(metadata), + } +} + // As 提取当前仓库约定的业务错误。 func As(err error) (*Error, bool) { if err == nil { @@ -234,3 +250,41 @@ func MessageOf(err error) string { return "internal error" } + +// MetadataOf 返回业务错误显式声明的安全 metadata 副本。 +// 未识别错误没有可安全暴露的结构化详情,必须返回 nil 而不是猜测原始错误内容。 +func MetadataOf(err error) map[string]string { + if typed, ok := As(err); ok { + return cloneMetadata(typed.Metadata) + } + return nil +} + +func normalizeMetadata(metadata map[string]string) map[string]string { + if len(metadata) == 0 { + return nil + } + normalized := make(map[string]string, len(metadata)) + for key, value := range metadata { + key = strings.TrimSpace(key) + if key == "" { + continue + } + normalized[key] = strings.TrimSpace(value) + } + if len(normalized) == 0 { + return nil + } + return normalized +} + +func cloneMetadata(metadata map[string]string) map[string]string { + if len(metadata) == 0 { + return nil + } + cloned := make(map[string]string, len(metadata)) + for key, value := range metadata { + cloned[key] = value + } + return cloned +} diff --git a/pkg/xerr/grpc.go b/pkg/xerr/grpc.go index d556ac38..4d8f7cd7 100644 --- a/pkg/xerr/grpc.go +++ b/pkg/xerr/grpc.go @@ -45,8 +45,9 @@ func ToGRPCError(err error) error { st := status.New(GRPCCode(domainErr.Code), domainErr.Message) detail := &errdetails.ErrorInfo{ - Reason: string(domainErr.Code), - Domain: errorDomain, + Reason: string(domainErr.Code), + Domain: errorDomain, + Metadata: cloneMetadata(domainErr.Metadata), } if withDetails, detailErr := st.WithDetails(detail); detailErr == nil { @@ -109,6 +110,25 @@ func ReasonFromGRPC(err error) Code { return codeFromGRPC(st.Code()) } +// MetadataFromGRPC 只读取本项目 ErrorInfo 中由 owner service 显式声明的安全字段。 +// 其它 detail/domain 不属于公开 HTTP 契约,gateway 不得把它们混入 envelope.data。 +func MetadataFromGRPC(err error) map[string]string { + if err == nil { + return nil + } + st, ok := status.FromError(err) + if !ok { + return nil + } + for _, detail := range st.Details() { + info, ok := detail.(*errdetails.ErrorInfo) + if ok && info.GetDomain() == errorDomain && info.GetReason() != "" { + return cloneMetadata(info.GetMetadata()) + } + } + return nil +} + // codeFromGRPC 是没有 ErrorInfo detail 时的降级映射。 func codeFromGRPC(code codes.Code) Code { switch code { diff --git a/scripts/apply-local-mysql-initdb.sh b/scripts/apply-local-mysql-initdb.sh index eab81850..d2857ebf 100755 --- a/scripts/apply-local-mysql-initdb.sh +++ b/scripts/apply-local-mysql-initdb.sh @@ -36,7 +36,10 @@ INCREMENTAL_MIGRATION_FILES=( "services/user-service/deploy/mysql/migrations/012_login_audit_client_version.sql" "services/user-service/deploy/mysql/migrations/017_cp_application_gift_coin_value.sql" "services/user-service/deploy/mysql/migrations/018_auth_refresh_token_rotation.sql" + "services/user-service/deploy/mysql/migrations/019_user_profile_anonymous_visits.sql" + "services/room-service/deploy/mysql/migrations/003_room_vip_media_and_decorations.sql" "services/wallet-service/deploy/mysql/migrations/002_resource_shop_price_tiers.sql" + "services/wallet-service/deploy/mysql/migrations/003_vip_resource_equipment_contract.sql" ) # Keep MySQL as the only required dependency for this script. Business services diff --git a/scripts/mysql/069_vip_resource_equipment_contract.sql b/scripts/mysql/069_vip_resource_equipment_contract.sql new file mode 100644 index 00000000..1c43c7b7 --- /dev/null +++ b/scripts/mysql/069_vip_resource_equipment_contract.sql @@ -0,0 +1,258 @@ +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_wallet; + +-- 装备幂等表是 append-only 小行写入;所有重放只按主键 (app_code,command_id) 点查, +-- 不修改 user_resource_equipment 主键,也不会为上线迁移扫描用户装备或 entitlement 大表。 +CREATE TABLE IF NOT EXISTS user_resource_equipment_commands ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + command_id VARCHAR(128) NOT NULL COMMENT '装备业务幂等 ID', + request_hash CHAR(64) NOT NULL COMMENT '用户、资源和指定 entitlement 的请求语义哈希', + user_id BIGINT NOT NULL COMMENT '操作用户 ID', + resource_id BIGINT NOT NULL COMMENT '目标资源 ID', + requested_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '客户端指定的 entitlement;空表示服务端选择', + result_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首个成功命中的 entitlement', + resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '首个成功结果的资源类型', + response_json JSON NULL COMMENT '首个成功 UserResourceEntitlement 回包快照', + created_at_ms BIGINT NOT NULL COMMENT '首次命令时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '结果完成时间,UTC epoch ms', + PRIMARY KEY (app_code, command_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备命令幂等事实表'; + +-- 配置矩阵很小,历史表按 admin config_version 追加;装备和 appearance 只按资源覆盖索引点查。 +-- 记录 program_type,避免 legacy_timed App 的普通资源被 tiered VIP 门禁误伤。 +CREATE TABLE IF NOT EXISTS vip_benefit_resource_binding_history ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + history_id CHAR(64) NOT NULL COMMENT '配置版本、阶段、等级和权益的稳定哈希', + config_version BIGINT NOT NULL COMMENT '写入该快照的 VIP 配置版本', + change_phase VARCHAR(16) NOT NULL COMMENT 'baseline/before/after', + program_type VARCHAR(32) NOT NULL COMMENT '快照时的 VIP program 类型', + level INT NOT NULL COMMENT 'VIP 等级', + benefit_code VARCHAR(64) NOT NULL COMMENT '权益编码', + benefit_status VARCHAR(32) NOT NULL COMMENT '权益快照状态', + trial_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '体验卡是否可用快照', + resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '当时绑定资源 ID;0 表示未绑定', + resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '当时要求的资源类型', + auto_equip BOOLEAN NOT NULL DEFAULT FALSE COMMENT '当时自动装备策略', + recorded_at_ms BIGINT NOT NULL COMMENT '记录时间,UTC epoch ms', + PRIMARY KEY (app_code, history_id), + UNIQUE KEY uk_vip_binding_history_snapshot (app_code, config_version, change_phase, level, benefit_code), + KEY idx_vip_binding_history_resource (app_code, resource_id, program_type, benefit_code, level) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 权益资源绑定不可变历史'; + +-- 装备门禁按单 App、单 resource_id 查询 active 绑定。该配置表规模远小于业务流水, +-- 仍显式补覆盖索引并使用在线 DDL,避免每次装备扫描完整权益矩阵。 +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_level_benefits' + AND INDEX_NAME = 'idx_vip_benefit_resource') = 0, + 'ALTER TABLE vip_level_benefits ADD INDEX idx_vip_benefit_resource (app_code, resource_id, status, benefit_code, level), ALGORITHM=INPLACE, LOCK=NONE', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @vip_resource_contract_now_ms := CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED); + +-- 在任何修正规则执行前保存旧映射;后续 admin 更新还会为每个新版本分别追加 before/after。 +INSERT IGNORE INTO vip_benefit_resource_binding_history ( + app_code, history_id, config_version, change_phase, program_type, level, + benefit_code, benefit_status, trial_enabled, resource_id, resource_type, + auto_equip, recorded_at_ms +) +SELECT b.app_code, + SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256), + p.config_version, 'baseline', p.program_type, b.level, b.benefit_code, + b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip, + @vip_resource_contract_now_ms +FROM vip_level_benefits AS b +JOIN vip_program_configs AS p ON p.app_code = b.app_code; + +-- tiered Fami 的房间视觉权益使用稳定映射:mic_skin -> mic_seat_animation、 +-- room_border -> room_border、colored_room_name -> room_name_style。三项都属于 decoration; +-- disabled 可以无素材,但若已有 resource_id 也不能保留错误类型或下架资源。 +UPDATE vip_level_benefits +SET benefit_type = 'decoration', + resource_type = CASE benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + END, + updated_at_ms = @vip_resource_contract_now_ms +WHERE app_code = 'fami' + AND benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') + AND ( + benefit_type <> 'decoration' + OR resource_type <> CASE benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + END + ); + +SET @vip_decoration_normalize_count := ROW_COUNT(); + +UPDATE vip_level_benefits AS benefit +LEFT JOIN resources AS resource + ON resource.app_code = benefit.app_code + AND resource.resource_id = benefit.resource_id + AND resource.status = 'active' + AND resource.resource_type = CASE benefit.benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + END +SET benefit.resource_id = 0, + benefit.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') + AND benefit.resource_id > 0 + AND resource.resource_id IS NULL; + +SET @vip_invalid_resource_clear_count := ROW_COUNT(); + +-- 本地现有 Fami VIP4..VIP8 资源组各自只有一个可核实的 active mic_seat_animation。 +-- 这里按稳定 group_code + 类型解析 ID,并用 HAVING COUNT(DISTINCT)=1 防止运营新增候选后误绑; +-- VIP9、room_border、room_name_style 当前没有真实目录资源,必须保持未配置,不能伪造 ID/URL。 +UPDATE vip_level_benefits AS benefit +JOIN ( + SELECT resource_group.group_code, MIN(resource.resource_id) AS resource_id + FROM resource_groups AS resource_group + JOIN resource_group_items AS group_item + ON group_item.app_code = resource_group.app_code + AND group_item.group_id = resource_group.group_id + AND group_item.item_type = 'resource' + JOIN resources AS resource + ON resource.app_code = group_item.app_code + AND resource.resource_id = group_item.resource_id + AND resource.resource_type = 'mic_seat_animation' + AND resource.status = 'active' + WHERE resource_group.app_code = 'fami' + AND resource_group.status = 'active' + AND resource_group.group_code IN ('VIP4', 'VIP5', 'VIP6', 'VIP7', 'VIP8') + GROUP BY resource_group.group_code + HAVING COUNT(DISTINCT resource.resource_id) = 1 +) AS mapping + ON mapping.group_code = CONCAT('VIP', benefit.level) +SET benefit.resource_id = mapping.resource_id, + benefit.resource_type = 'mic_seat_animation', + benefit.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.level BETWEEN 4 AND 8 + AND benefit.benefit_code = 'mic_skin' + AND benefit.status = 'active' + AND benefit.resource_id = 0; + +SET @vip_mic_skin_bind_count := ROW_COUNT(); + +-- 只给“已经被 active mic_skin 绑定”的真实麦座动画补显式 format:先信任后台已有的 +-- 合法 animation_format,否则只从实际 animation/asset URL 的明确后缀推导。这个更新 +-- 通过权益配置小表圈定 resource_id,再按 resources 主键点查,不会扫描或修改未绑定资源。 +UPDATE vip_level_benefits AS benefit +JOIN resources AS resource FORCE INDEX (PRIMARY) + ON resource.resource_id = benefit.resource_id + AND resource.app_code = benefit.app_code + AND resource.resource_type = 'mic_seat_animation' + AND resource.status = 'active' +SET resource.metadata_json = JSON_SET( + COALESCE(resource.metadata_json, JSON_OBJECT()), + '$.format', + CASE + WHEN LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) + IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + THEN LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')))) + WHEN LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1)) + REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' + THEN CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1)) + WHEN 'jpg' THEN 'jpeg' + ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1)) + END + ELSE CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1)) + WHEN 'jpg' THEN 'jpeg' + ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1)) + END + END + ), + resource.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.benefit_code = 'mic_skin' + AND benefit.status = 'active' + AND benefit.resource_type = 'mic_seat_animation' + AND benefit.resource_id > 0 + AND (TRIM(resource.asset_url) <> '' OR TRIM(resource.animation_url) <> '') + AND TRIM(resource.preview_url) <> '' + AND LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), ''))) + NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + AND ( + LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) + IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1)) + REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' + OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1)) + REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' + ); + +SET @vip_mic_format_backfill_count := ROW_COUNT(); + +-- active 必须能联结到同 App、active、精确类型的真实资源。麦座动画和房间边框还必须 +-- 同时具备素材、preview、合法 format,且可选 animation_format 一旦配置就必须一致。 +-- 缺少真实配置时只禁用权益,不创建占位资源,也不猜测素材 URL。 +UPDATE vip_level_benefits AS benefit +LEFT JOIN resources AS resource + ON resource.app_code = benefit.app_code + AND resource.resource_id = benefit.resource_id + AND resource.status = 'active' + AND resource.resource_type = benefit.resource_type +SET benefit.status = 'disabled', + benefit.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') + AND benefit.status = 'active' + AND ( + resource.resource_id IS NULL + OR ( + benefit.resource_type IN ('mic_seat_animation', 'room_border') + AND ( + (TRIM(resource.asset_url) = '' AND TRIM(resource.animation_url) = '') + OR TRIM(resource.preview_url) = '' + OR LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), ''))) + NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + OR ( + LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) <> '' + AND LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')))) + <> LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')))) + ) + ) + ) + ); + +SET @vip_decoration_disable_count := ROW_COUNT(); + +-- config_version 只在首次真实回填时递增;迁移重复执行不会制造无意义版本变化。 +UPDATE vip_program_configs +SET config_version = config_version + 1, + updated_at_ms = @vip_resource_contract_now_ms +WHERE app_code = 'fami' + AND ( + @vip_decoration_normalize_count + + @vip_invalid_resource_clear_count + + @vip_mic_skin_bind_count + + @vip_mic_format_backfill_count + + @vip_decoration_disable_count + ) > 0; + +-- 本次迁移若提升了 config_version,立即写入新版本的最终 baseline。这样下一次重放开头的 +-- baseline INSERT 会命中唯一键,不会仅因为上次版本递增再追加一轮完全相同的历史快照。 +INSERT IGNORE INTO vip_benefit_resource_binding_history ( + app_code, history_id, config_version, change_phase, program_type, level, + benefit_code, benefit_status, trial_enabled, resource_id, resource_type, + auto_equip, recorded_at_ms +) +SELECT b.app_code, + SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256), + p.config_version, 'baseline', p.program_type, b.level, b.benefit_code, + b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip, + @vip_resource_contract_now_ms +FROM vip_level_benefits AS b +JOIN vip_program_configs AS p ON p.app_code = b.app_code; diff --git a/scripts/mysql/071_room_vip_media_and_decorations.sql b/scripts/mysql/071_room_vip_media_and_decorations.sql new file mode 100644 index 00000000..4bc69d5f --- /dev/null +++ b/scripts/mysql/071_room_vip_media_and_decorations.sql @@ -0,0 +1,156 @@ +-- 与 room-service/deploy/mysql/migrations/003_room_vip_media_and_decorations.sql 保持同一可重入结构。 +-- 性能边界:INSTANT 列仍会短暂申请 metadata lock;image_url 扩容及唯一索引在线扫描低频素材表,建议低峰执行。 + +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_room; + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS hyapp_migrate_071_room_vip_media_decorations$$ +CREATE PROCEDURE hyapp_migrate_071_room_vip_media_decorations() +BEGIN + DECLARE duplicate_groups BIGINT DEFAULT 0; + DECLARE oversized_urls BIGINT DEFAULT 0; + + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'room_background_images') THEN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'image_url' + AND (data_type <> 'varchar' OR character_maximum_length <> 1024) + ) THEN + SELECT COUNT(*) INTO oversized_urls FROM room_background_images WHERE CHAR_LENGTH(image_url) > 1024; + IF oversized_urls > 0 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'room_background_images.image_url contains values longer than 1024'; + END IF; + ALTER TABLE room_background_images MODIFY COLUMN image_url VARCHAR(1024) NOT NULL, ALGORITHM=INPLACE, LOCK=NONE; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'command_id') THEN + ALTER TABLE room_background_images ADD COLUMN command_id VARCHAR(128) NULL COMMENT '新版素材保存幂等键;历史行保持 NULL', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'object_key') THEN + ALTER TABLE room_background_images ADD COLUMN object_key VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'COS 对象 key', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'content_type') THEN + ALTER TABLE room_background_images ADD COLUMN content_type VARCHAR(64) NOT NULL DEFAULT '' COMMENT '服务端嗅探 MIME', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'media_format') THEN + ALTER TABLE room_background_images ADD COLUMN media_format VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'jpeg/png/gif/webp', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'size_bytes') THEN + ALTER TABLE room_background_images ADD COLUMN size_bytes BIGINT NOT NULL DEFAULT 0 COMMENT '文件字节数', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'width') THEN + ALTER TABLE room_background_images ADD COLUMN width INT NOT NULL DEFAULT 0 COMMENT '像素宽度', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'height') THEN + ALTER TABLE room_background_images ADD COLUMN height INT NOT NULL DEFAULT 0 COMMENT '像素高度', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'animated') THEN + ALTER TABLE room_background_images ADD COLUMN animated BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否动图', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'frame_count') THEN + ALTER TABLE room_background_images ADD COLUMN frame_count INT NOT NULL DEFAULT 0 COMMENT '帧数', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'duration_ms') THEN + ALTER TABLE room_background_images ADD COLUMN duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '动图总时长', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'poster_url') THEN + ALTER TABLE room_background_images ADD COLUMN poster_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选首帧 URL', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'thumbnail_url') THEN + ALTER TABLE room_background_images ADD COLUMN thumbnail_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选缩略图 URL', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'sha256') THEN + ALTER TABLE room_background_images ADD COLUMN sha256 CHAR(64) NOT NULL DEFAULT '' COMMENT '原文件 SHA-256', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'media_status') THEN + ALTER TABLE room_background_images ADD COLUMN media_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'active 表示已通过专用上传入口', ALGORITHM=INSTANT; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND index_name = 'uk_room_background_command' + ) THEN + SELECT COUNT(*) INTO duplicate_groups + FROM ( + SELECT 1 FROM room_background_images + WHERE command_id IS NOT NULL + GROUP BY app_code, room_id, command_id + HAVING COUNT(*) > 1 + LIMIT 1 + ) AS duplicate_command_groups; + IF duplicate_groups > 0 THEN + SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'duplicate room background command_id blocks unique index'; + END IF; + ALTER TABLE room_background_images ADD UNIQUE KEY uk_room_background_command (app_code, room_id, command_id), ALGORITHM=INPLACE, LOCK=NONE; + END IF; + END IF; + + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'room_list_entries') THEN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_list_entries' AND column_name = 'room_border_json') THEN + ALTER TABLE room_list_entries ADD COLUMN room_border_json JSON NULL COMMENT '当前未过期房间边框资源快照', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_list_entries' AND column_name = 'room_name_style_json') THEN + ALTER TABLE room_list_entries ADD COLUMN room_name_style_json JSON NULL COMMENT '当前未过期彩色房名资源快照', ALGORITHM=INSTANT; + END IF; + END IF; +END$$ + +CALL hyapp_migrate_071_room_vip_media_decorations()$$ +DROP PROCEDURE IF EXISTS hyapp_migrate_071_room_vip_media_decorations$$ + +DELIMITER ; + +CREATE TABLE IF NOT EXISTS room_media_upload_registrations ( + app_code VARCHAR(32) NOT NULL, + room_id VARCHAR(64) NOT NULL, + command_id VARCHAR(128) NOT NULL, + actor_user_id BIGINT NOT NULL, + purpose VARCHAR(32) NOT NULL, + media_payload_sha256 CHAR(64) NOT NULL, + expected_object_key VARCHAR(512) NOT NULL, + object_url VARCHAR(1024) NOT NULL DEFAULT '', + status VARCHAR(32) NOT NULL DEFAULT 'authorized', + content_type VARCHAR(64) NOT NULL, + media_format VARCHAR(16) NOT NULL, + size_bytes BIGINT NOT NULL, + width INT NOT NULL, + height INT NOT NULL, + animated BOOLEAN NOT NULL DEFAULT FALSE, + frame_count INT NOT NULL, + duration_ms BIGINT NOT NULL DEFAULT 0, + sha256 CHAR(64) NOT NULL, + authorized_room_version BIGINT NOT NULL, + created_at_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, room_id, command_id), + UNIQUE KEY uk_room_media_upload_object (app_code, expected_object_key), + KEY idx_room_media_upload_status_created (app_code, status, created_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间专用媒体上传授权与完成事实'; + +CREATE TABLE IF NOT EXISTS room_decoration_assignments ( + app_code VARCHAR(32) NOT NULL, + room_id VARCHAR(64) NOT NULL, + decoration_type VARCHAR(32) NOT NULL, + owner_user_id BIGINT NOT NULL, + benefit_code VARCHAR(64) NOT NULL, + resource_id BIGINT NOT NULL, + entitlement_id VARCHAR(128) NOT NULL DEFAULT '', + expires_at_ms BIGINT NOT NULL DEFAULT 0, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, room_id, decoration_type), + KEY idx_room_decoration_resource (app_code, resource_id, room_id, decoration_type), + KEY idx_room_decoration_owner (app_code, owner_user_id, room_id, decoration_type), + KEY idx_room_decoration_expiry (app_code, expires_at_ms, room_id, decoration_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间当前 VIP 装扮反向索引'; + +CREATE TABLE IF NOT EXISTS room_wallet_event_consumption ( + app_code VARCHAR(32) NOT NULL, + event_id VARCHAR(128) NOT NULL, + event_type VARCHAR(64) NOT NULL, + consumed_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, event_id), + KEY idx_room_wallet_event_consumed (app_code, consumed_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='room-service 消费 wallet 装扮事实的本地幂等表'; diff --git a/server/admin/internal/integration/luckygiftadmin/client.go b/server/admin/internal/integration/luckygiftadmin/client.go index 8ccaa21d..fb13f07b 100644 --- a/server/admin/internal/integration/luckygiftadmin/client.go +++ b/server/admin/internal/integration/luckygiftadmin/client.go @@ -14,6 +14,8 @@ type Client interface { GetLuckyGiftConfig(ctx context.Context, req *luckygiftv1.GetLuckyGiftConfigRequest) (*luckygiftv1.GetLuckyGiftConfigResponse, error) UpsertLuckyGiftConfig(ctx context.Context, req *luckygiftv1.UpsertLuckyGiftConfigRequest) (*luckygiftv1.UpsertLuckyGiftConfigResponse, error) ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigsRequest) (*luckygiftv1.ListLuckyGiftConfigsResponse, error) + ListLuckyGiftConfigVersions(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigVersionsRequest) (*luckygiftv1.ListLuckyGiftConfigVersionsResponse, error) + RollbackLuckyGiftConfig(ctx context.Context, req *luckygiftv1.RollbackLuckyGiftConfigRequest) (*luckygiftv1.RollbackLuckyGiftConfigResponse, error) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) GetLuckyGiftDrawSummary(ctx context.Context, req *luckygiftv1.GetLuckyGiftDrawSummaryRequest) (*luckygiftv1.GetLuckyGiftDrawSummaryResponse, error) ListLuckyGiftUserProfiles(ctx context.Context, req *luckygiftv1.ListLuckyGiftUserProfilesRequest) (*luckygiftv1.ListLuckyGiftUserProfilesResponse, error) @@ -48,6 +50,14 @@ func (c *GRPCClient) ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1. return c.client.ListLuckyGiftConfigs(ctx, req) } +func (c *GRPCClient) ListLuckyGiftConfigVersions(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigVersionsRequest) (*luckygiftv1.ListLuckyGiftConfigVersionsResponse, error) { + return c.client.ListLuckyGiftConfigVersions(ctx, req) +} + +func (c *GRPCClient) RollbackLuckyGiftConfig(ctx context.Context, req *luckygiftv1.RollbackLuckyGiftConfigRequest) (*luckygiftv1.RollbackLuckyGiftConfigResponse, error) { + return c.client.RollbackLuckyGiftConfig(ctx, req) +} + func (c *GRPCClient) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) { return c.client.ListLuckyGiftDraws(ctx, req) } diff --git a/server/admin/internal/modules/luckygift/handler.go b/server/admin/internal/modules/luckygift/handler.go index 33a5557f..aabff3a0 100644 --- a/server/admin/internal/modules/luckygift/handler.go +++ b/server/admin/internal/modules/luckygift/handler.go @@ -95,6 +95,10 @@ type configRequest struct { Stages []stageDTO `json:"stages"` } +type configRollbackRequest struct { + TargetRuleVersion int64 `json:"target_rule_version"` +} + // configUpsertRequest 只服务 HTTP 写入口。滚动发布期间旧前端仍可能发送 day/72h 字段, // 因此输入 DTO 明确接收两代名字;configRequest 继续只暴露 canonical JSON,保证 GET 和写入响应不会双写字段。 type configUpsertRequest struct { @@ -297,6 +301,67 @@ func (h *Handler) ListLuckyGiftConfigs(c *gin.Context) { response.OK(c, items) } +func (h *Handler) ListLuckyGiftConfigVersions(c *gin.Context) { + appCode := strings.TrimSpace(c.Query("app_code")) + poolID := strings.TrimSpace(c.Query("pool_id")) + if appCode == "" || poolID == "" { + response.BadRequest(c, "app_code and pool_id are required") + return + } + ctx, cancel := h.luckyGiftContext(c) + defer cancel() + resp, err := h.luckyGift.ListLuckyGiftConfigVersions(ctx, &luckygiftv1.ListLuckyGiftConfigVersionsRequest{ + Meta: h.metaForApp(c, appCode), + PoolId: poolID, + }) + if err != nil { + h.writeConfigRollbackError(c, err, "获取幸运礼物历史版本失败") + return + } + items := make([]configDTO, 0, len(resp.GetConfigs())) + for _, config := range resp.GetConfigs() { + items = append(items, configFromProto(config)) + } + response.OK(c, items) +} + +func (h *Handler) RollbackLuckyGiftConfig(c *gin.Context) { + appCode := strings.TrimSpace(c.Query("app_code")) + poolID := strings.TrimSpace(c.Query("pool_id")) + if appCode == "" || poolID == "" { + response.BadRequest(c, "app_code and pool_id are required") + return + } + var input configRollbackRequest + decoder := json.NewDecoder(c.Request.Body) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&input); err != nil || input.TargetRuleVersion <= 0 { + response.BadRequest(c, "回滚目标版本不正确") + return + } + ctx, cancel := h.luckyGiftContext(c) + defer cancel() + resp, err := h.luckyGift.RollbackLuckyGiftConfig(ctx, &luckygiftv1.RollbackLuckyGiftConfigRequest{ + Meta: h.metaForApp(c, appCode), + PoolId: poolID, + TargetRuleVersion: input.TargetRuleVersion, + OperatorAdminId: int64(middleware.CurrentUserID(c)), + }) + if err != nil { + h.writeConfigRollbackError(c, err, "回滚幸运礼物版本失败") + return + } + item := configFromProto(resp.GetConfig()) + shared.OperationLogWithResourceID( + c, h.audit, "rollback-lucky-gift-config", "lucky_gift_rule_versions", fmt.Sprintf("%s/%s/v%d", appCode, poolID, item.RuleVersion), "success", + fmt.Sprintf("source_rule_version=v%d new_rule_version=v%d", resp.GetSourceRuleVersion(), item.RuleVersion), + ) + response.OK(c, gin.H{ + "source_rule_version": resp.GetSourceRuleVersion(), + "config": item, + }) +} + func (h *Handler) ListLuckyGiftDraws(c *gin.Context) { if strings.TrimSpace(c.Query("app_code")) == "" { response.BadRequest(c, "app_code is required") @@ -444,6 +509,20 @@ func (h *Handler) writePoolAdjustmentError(c *gin.Context, err error) { } } +func (h *Handler) writeConfigRollbackError(c *gin.Context, err error, fallback string) { + status := grpcstatus.Convert(err) + switch status.Code() { + case codes.InvalidArgument: + response.BadRequest(c, status.Message()) + case codes.NotFound: + response.NotFound(c, status.Message()) + case codes.AlreadyExists, codes.FailedPrecondition, codes.Aborted: + response.Conflict(c, status.Message()) + default: + response.ServerError(c, fallback) + } +} + func (h *Handler) meta(c *gin.Context) *luckygiftv1.RequestMeta { return h.metaForApp(c, "") } diff --git a/server/admin/internal/modules/opscenter/routes.go b/server/admin/internal/modules/opscenter/routes.go index a4e7b3d2..abfa83fb 100644 --- a/server/admin/internal/modules/opscenter/routes.go +++ b/server/admin/internal/modules/opscenter/routes.go @@ -22,6 +22,8 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) { group.GET("/lucky-gifts/config", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftConfig) group.PUT("/lucky-gifts/config", middleware.RequirePermission("lucky-gift:update"), h.luckyGift.UpsertLuckyGiftConfig) group.GET("/lucky-gifts/configs", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftConfigs) + group.GET("/lucky-gifts/config/versions", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftConfigVersions) + group.POST("/lucky-gifts/config/rollback", middleware.RequirePermission("lucky-gift:update"), h.luckyGift.RollbackLuckyGiftConfig) group.GET("/lucky-gifts/draws", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftDraws) group.GET("/lucky-gifts/summary", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftDrawSummary) group.GET("/lucky-gifts/user-profiles", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftUserProfiles) diff --git a/server/admin/internal/modules/resource/handler.go b/server/admin/internal/modules/resource/handler.go index e8ddd647..791b2eb3 100644 --- a/server/admin/internal/modules/resource/handler.go +++ b/server/admin/internal/modules/resource/handler.go @@ -141,6 +141,10 @@ func (h *Handler) CreateResource(c *gin.Context) { response.BadRequest(c, err.Error()) return } + if err := validateRoomDecorationResourceMaterial(req); err != nil { + response.BadRequest(c, err.Error()) + return + } if err := validateResourceMetadata(req); err != nil { response.BadRequest(c, err.Error()) return @@ -257,6 +261,10 @@ func (h *Handler) UpdateResource(c *gin.Context) { response.BadRequest(c, err.Error()) return } + if err := validateRoomDecorationResourceMaterial(req); err != nil { + response.BadRequest(c, err.Error()) + return + } if err := validateResourceMetadata(req); err != nil { response.BadRequest(c, err.Error()) return diff --git a/server/admin/internal/modules/resource/request.go b/server/admin/internal/modules/resource/request.go index b96e45eb..67fa4da8 100644 --- a/server/admin/internal/modules/resource/request.go +++ b/server/admin/internal/modules/resource/request.go @@ -3,6 +3,8 @@ package resource import ( "encoding/json" "fmt" + "math" + "regexp" "strconv" "strings" "time" @@ -22,10 +24,19 @@ const ( resourceTypeAvatarFrame = "avatar_frame" resourceTypeBadge = "badge" resourceTypeEmojiPack = "emoji_pack" + resourceTypeMicAnimation = "mic_seat_animation" resourceTypeProfileCard = "profile_card" + resourceTypeRoomBorder = "room_border" + resourceTypeRoomName = "room_name_style" resourceTypeVIPTrialCard = "vip_trial_card" ) +var roomNameStyleColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$`) + +var vipVisualResourceFormats = map[string]struct{}{ + "gif": {}, "jpeg": {}, "mp4": {}, "pag": {}, "png": {}, "svga": {}, "webp": {}, +} + const ( resourceAnimationFormatMP4 = "mp4" ) @@ -126,9 +137,13 @@ type badgeMetadataPayload struct { type resourceMetadataPayload struct { AnimationFormat string `json:"animation_format,omitempty"` AvatarFrameKind string `json:"avatar_frame_kind,omitempty"` + Format string `json:"format,omitempty"` LevelTrack string `json:"level_track,omitempty"` MP4AlphaLayout *mp4AlphaLayoutMetadataPayload `json:"mp4_alpha_layout,omitempty"` ProfileCardLayout *profileCardLayoutMetadataPayload `json:"profile_card_layout,omitempty"` + TextColors []string `json:"text_colors,omitempty"` + Stops []float64 `json:"stops,omitempty"` + AngleDegrees *float64 `json:"angle_degrees,omitempty"` } type mp4AlphaLayoutMetadataPayload struct { @@ -644,8 +659,25 @@ func validateResourceMetadata(req resourceRequest) error { return fmt.Errorf("资源元数据格式不正确") } if payload == nil { + if resourceType == resourceTypeRoomName { + return fmt.Errorf("请配置房间名称渐变样式") + } return nil } + if resourceType == resourceTypeRoomName { + if err := validateRoomNameStyleMetadata(payload); err != nil { + return err + } + } + if resourceRequiresExplicitVisualFormat(resourceType) { + format := strings.ToLower(strings.TrimSpace(payload.Format)) + if _, allowed := vipVisualResourceFormats[format]; !allowed { + return fmt.Errorf("视觉资源素材格式不支持") + } + if animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat)); animationFormat != "" && animationFormat != format { + return fmt.Errorf("视觉资源素材格式与动效格式不一致") + } + } if rawFormat, ok := fields["animation_format"]; ok && len(rawFormat) > 0 { animationFormat := strings.ToLower(strings.TrimSpace(payload.AnimationFormat)) if animationFormat != "" && animationFormat != resourceAnimationFormatMP4 { @@ -683,6 +715,39 @@ func resourceGrantStrategy(resourceType string) string { } } +func validateRoomDecorationResourceMaterial(req resourceRequest) error { + resourceType := normalizeResourceType(req.ResourceType) + if !resourceRequiresExplicitVisualFormat(resourceType) { + return nil + } + // 麦位动效和房间边框都是实际视觉资源,不能创建只有编码的占位行; + // 样式型 room_name_style 则由 metadata 独立渲染。 + if strings.TrimSpace(req.AssetURL) == "" && strings.TrimSpace(req.AnimationURL) == "" { + return fmt.Errorf("请上传视觉资源素材") + } + if strings.TrimSpace(req.PreviewURL) == "" { + return fmt.Errorf("请上传视觉资源预览图") + } + payload, _, err := parseResourceMetadataPayload(req.MetadataJSON) + if err != nil || payload == nil { + return fmt.Errorf("请配置视觉资源素材格式") + } + format := strings.ToLower(strings.TrimSpace(payload.Format)) + if _, allowed := vipVisualResourceFormats[format]; !allowed { + return fmt.Errorf("视觉资源素材格式不支持") + } + return nil +} + +func resourceRequiresExplicitVisualFormat(resourceType string) bool { + switch normalizeResourceType(resourceType) { + case resourceTypeMicAnimation, resourceTypeRoomBorder: + return true + default: + return false + } +} + func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON string, badgeKind string, levelTrack string, avatarFrameKind string) string { if resourceType == resourceTypeBadge { return badgeMetadataJSON(badgeForm, badgeKind, levelTrack) @@ -707,6 +772,14 @@ func resourceMetadataJSON(resourceType string, badgeForm string, metadataJSON st payload.LevelTrack = normalizeLevelTrack(levelTrack) } } + if resourceType == resourceTypeRoomName || resourceRequiresExplicitVisualFormat(resourceType) { + payload.Format = strings.ToLower(strings.TrimSpace(rawPayload.Format)) + } + if resourceType == resourceTypeRoomName { + payload.TextColors = append([]string(nil), rawPayload.TextColors...) + payload.Stops = append([]float64(nil), rawPayload.Stops...) + payload.AngleDegrees = rawPayload.AngleDegrees + } if resourceAllowsAnimationMetadata(resourceType) { if layout, err := sanitizeMP4AlphaLayoutMetadata(rawPayload.MP4AlphaLayout); err == nil && layout != nil && layout.Confirmed { payload.AnimationFormat = resourceAnimationFormatMP4 @@ -777,7 +850,7 @@ func parseResourceMetadataPayload(metadataJSON string) (*resourceMetadataPayload } func marshalResourceMetadataPayload(payload resourceMetadataPayload) string { - if payload.AnimationFormat == "" && payload.AvatarFrameKind == "" && payload.LevelTrack == "" && payload.MP4AlphaLayout == nil && payload.ProfileCardLayout == nil { + if payload.AnimationFormat == "" && payload.AvatarFrameKind == "" && payload.Format == "" && payload.LevelTrack == "" && payload.MP4AlphaLayout == nil && payload.ProfileCardLayout == nil && len(payload.TextColors) == 0 && len(payload.Stops) == 0 && payload.AngleDegrees == nil { return "{}" } body, err := json.Marshal(payload) @@ -875,13 +948,40 @@ func normalizeMP4AlphaLayout(value string) string { func resourceAllowsAnimationMetadata(resourceType string) bool { switch resourceType { - case "gift", "vehicle", "avatar_frame", "chat_bubble", "floating_screen", resourceTypeProfileCard: + case "gift", "vehicle", "avatar_frame", "chat_bubble", "floating_screen", resourceTypeProfileCard, resourceTypeRoomBorder: return true default: return strings.HasPrefix(resourceType, "mic_seat_") } } +func validateRoomNameStyleMetadata(payload *resourceMetadataPayload) error { + if payload == nil || len(payload.TextColors) == 0 || len(payload.TextColors) > 8 { + return fmt.Errorf("房名渐变颜色必须配置 1 至 8 个") + } + for index, color := range payload.TextColors { + color = strings.TrimSpace(color) + if !roomNameStyleColorPattern.MatchString(color) { + return fmt.Errorf("房名渐变颜色必须使用 #RRGGBB 或 #RRGGBBAA") + } + payload.TextColors[index] = color + } + if len(payload.Stops) != 0 && len(payload.Stops) != len(payload.TextColors) { + return fmt.Errorf("房名渐变位置数量必须与颜色一致") + } + previous := -1.0 + for _, stop := range payload.Stops { + if math.IsNaN(stop) || math.IsInf(stop, 0) || stop < 0 || stop > 1 || stop < previous { + return fmt.Errorf("房名渐变位置必须按 0 至 1 递增") + } + previous = stop + } + if payload.AngleDegrees != nil && (math.IsNaN(*payload.AngleDegrees) || math.IsInf(*payload.AngleDegrees, 0)) { + return fmt.Errorf("房名渐变角度不正确") + } + return nil +} + func sanitizeProfileCardLayoutMetadata(layout *profileCardLayoutMetadataPayload) *profileCardLayoutMetadataPayload { if layout == nil { return nil diff --git a/services/gateway-service/internal/client/room_client.go b/services/gateway-service/internal/client/room_client.go index 5842670a..a4201f03 100644 --- a/services/gateway-service/internal/client/room_client.go +++ b/services/gateway-service/internal/client/room_client.go @@ -43,6 +43,18 @@ type RoomGuardClient interface { ResolveRoomAppCode(ctx context.Context, req *roomv1.ResolveRoomAppCodeRequest) (*roomv1.ResolveRoomAppCodeResponse, error) } +// RoomVIPFeatureClient 是新版房间 VIP 展示能力的窄接口,避免扩大旧 RoomClient fake。 +type RoomVIPFeatureClient interface { + ApplyRoomDecoration(ctx context.Context, req *roomv1.ApplyRoomDecorationRequest) (*roomv1.ApplyRoomDecorationResponse, error) + SendRoomImage(ctx context.Context, req *roomv1.SendRoomImageRequest) (*roomv1.SendRoomImageResponse, error) +} + +// RoomMediaGuardClient 在 COS 写入前向 room owner 校验上传权限。 +type RoomMediaGuardClient interface { + AuthorizeRoomMediaUpload(ctx context.Context, req *roomv1.AuthorizeRoomMediaUploadRequest) (*roomv1.AuthorizeRoomMediaUploadResponse, error) + CompleteRoomMediaUpload(ctx context.Context, req *roomv1.CompleteRoomMediaUploadRequest) (*roomv1.CompleteRoomMediaUploadResponse, error) +} + // RoomQueryClient 抽象 gateway 对 room-service 读模型查询的依赖。 type RoomQueryClient interface { ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) @@ -108,6 +120,14 @@ func (c *grpcRoomClient) SetRoomBackground(ctx context.Context, req *roomv1.SetR return c.client.SetRoomBackground(ctx, req) } +func (c *grpcRoomClient) ApplyRoomDecoration(ctx context.Context, req *roomv1.ApplyRoomDecorationRequest) (*roomv1.ApplyRoomDecorationResponse, error) { + return c.client.ApplyRoomDecoration(ctx, req) +} + +func (c *grpcRoomClient) SendRoomImage(ctx context.Context, req *roomv1.SendRoomImageRequest) (*roomv1.SendRoomImageResponse, error) { + return c.client.SendRoomImage(ctx, req) +} + func (c *grpcRoomClient) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) { return c.client.JoinRoom(ctx, req) } @@ -200,6 +220,14 @@ func (c *grpcRoomGuardClient) ResolveRoomAppCode(ctx context.Context, req *roomv return c.client.ResolveRoomAppCode(ctx, req) } +func (c *grpcRoomGuardClient) AuthorizeRoomMediaUpload(ctx context.Context, req *roomv1.AuthorizeRoomMediaUploadRequest) (*roomv1.AuthorizeRoomMediaUploadResponse, error) { + return c.client.AuthorizeRoomMediaUpload(ctx, req) +} + +func (c *grpcRoomGuardClient) CompleteRoomMediaUpload(ctx context.Context, req *roomv1.CompleteRoomMediaUploadRequest) (*roomv1.CompleteRoomMediaUploadResponse, error) { + return c.client.CompleteRoomMediaUpload(ctx, req) +} + func (c *grpcRoomQueryClient) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) { return c.client.ListRooms(ctx, req) } @@ -228,6 +256,11 @@ func (c *grpcRoomQueryClient) GetCurrentRoom(ctx context.Context, req *roomv1.Ge return c.client.GetCurrentRoom(ctx, req) } +// BatchGetUserVoiceRoomPresences 保持为具体 client 能力;只有个人资料页入口依赖它,避免扩大所有房间查询 mock 的接口面。 +func (c *grpcRoomQueryClient) BatchGetUserVoiceRoomPresences(ctx context.Context, req *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) { + return c.client.BatchGetUserVoiceRoomPresences(ctx, req) +} + func (c *grpcRoomQueryClient) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) { return c.client.GetRoomSnapshot(ctx, req) } diff --git a/services/gateway-service/internal/transport/http/httpkit/response.go b/services/gateway-service/internal/transport/http/httpkit/response.go index 1ccc18d0..5e57cbee 100644 --- a/services/gateway-service/internal/transport/http/httpkit/response.go +++ b/services/gateway-service/internal/transport/http/httpkit/response.go @@ -101,10 +101,17 @@ func WriteOK(writer http.ResponseWriter, request *http.Request, data any) { // WriteError 写失败 envelope。 // 错误 message 保持稳定、短小,排障依赖 request_id 追日志,而不是把内部错误直接暴露给客户端。 func WriteError(writer http.ResponseWriter, request *http.Request, statusCode int, code string, message string) { + WriteErrorData(writer, request, statusCode, code, message, nil) +} + +// WriteErrorData 写入客户端可执行的结构化错误详情。 +// data 只能来自业务错误的显式安全 metadata 或 handler 已判定的公开状态,不能放原始 err。 +func WriteErrorData(writer http.ResponseWriter, request *http.Request, statusCode int, code string, message string, data any) { WriteEnvelope(writer, statusCode, ResponseEnvelope{ Code: code, Message: message, RequestID: ResponseRequestID(request), + Data: data, }) } @@ -127,7 +134,30 @@ func WriteRPCError(writer http.ResponseWriter, request *http.Request, err error) message = detail } } - WriteError(writer, request, statusCode, code, message) + WriteErrorData(writer, request, statusCode, code, message, rpcErrorData(err)) +} + +// rpcErrorData 将 ErrorInfo.metadata 转成 HTTP JSON data,并只对已登记的等级字段做数值化。 +// google.rpc 的 metadata wire 类型固定为 string;这里显式 ParseInt 后,Flutter 才能按整数 +// 比较 required_level/current_effective_level,其余字段继续保持稳定字符串语义。 +func rpcErrorData(err error) map[string]any { + metadata := xerr.MetadataFromGRPC(err) + if len(metadata) == 0 { + return nil + } + data := make(map[string]any, len(metadata)) + for key, value := range metadata { + switch key { + case "required_level", "current_effective_level": + level, parseErr := strconv.ParseInt(value, 10, 32) + if parseErr == nil { + data[key] = level + continue + } + } + data[key] = value + } + return data } // Write 统一把内部 gRPC 调用结果转换为 HTTP JSON 响应。 diff --git a/services/gateway-service/internal/transport/http/httproutes/router.go b/services/gateway-service/internal/transport/http/httproutes/router.go index c77c0f71..90f504c7 100644 --- a/services/gateway-service/internal/transport/http/httproutes/router.go +++ b/services/gateway-service/internal/transport/http/httproutes/router.go @@ -75,6 +75,7 @@ type UserHandlers struct { ResolveDisplayUserID http.HandlerFunc BatchUserProfiles http.HandlerFunc BatchRoomDisplayProfiles http.HandlerFunc + BatchUserVoiceRoomPresences http.HandlerFunc GetMyOverview http.HandlerFunc GetMyInviteOverview http.HandlerFunc GetMyInviteReferrer http.HandlerFunc @@ -179,6 +180,10 @@ type RoomHandlers struct { SaveRoomBackground http.HandlerFunc ListRoomBackgrounds http.HandlerFunc SetRoomBackground http.HandlerFunc + UploadRoomBackground http.HandlerFunc + UploadRoomImage http.HandlerFunc + ApplyRoomDecoration http.HandlerFunc + SendRoomImage http.HandlerFunc JoinRoom http.HandlerFunc RoomHeartbeat http.HandlerFunc LeaveRoom http.HandlerFunc @@ -469,6 +474,7 @@ func (r routes) registerUserRoutes() { r.public("/users/by-id/{user_id}/appearance", http.MethodGet, h.GetUserAppearance) r.profile("/users/profiles:batch", "", h.BatchUserProfiles) r.profile("/users/room-display-profiles:batch", http.MethodGet, h.BatchRoomDisplayProfiles) + r.profile("/users/voice-room-presence:batch", http.MethodGet, h.BatchUserVoiceRoomPresences) r.profile("/users/me/overview", "", h.GetMyOverview) r.profile("/users/me/invite-overview", http.MethodGet, h.GetMyInviteOverview) r.profile("/users/me/invite-referrer", http.MethodGet, h.GetMyInviteReferrer) @@ -568,6 +574,10 @@ func (r routes) registerRoomRoutes() { r.profile("/rooms/{room_id}/rocket", http.MethodGet, h.GetRoomRocket) r.profile("/rooms/{room_id}/follow", "", h.FollowRoom) r.profile("/rooms/{room_id}/backgrounds", http.MethodGet, h.ListRoomBackgrounds) + r.profile("/rooms/{room_id}/backgrounds/upload", http.MethodPost, h.UploadRoomBackground) + r.profile("/rooms/{room_id}/messages/images/upload", http.MethodPost, h.UploadRoomImage) + r.profile("/rooms/{room_id}/messages/image", http.MethodPost, h.SendRoomImage) + r.profile("/rooms/{room_id}/decorations/apply", http.MethodPost, h.ApplyRoomDecoration) r.profile("/rooms/create", http.MethodPost, h.CreateRoom) r.profile("/rooms/profile/update", http.MethodPost, h.UpdateRoomProfile) r.profile("/rooms/backgrounds/save", http.MethodPost, h.SaveRoomBackground) diff --git a/services/gateway-service/internal/transport/http/resourceapi/resource_handler.go b/services/gateway-service/internal/transport/http/resourceapi/resource_handler.go index d3148297..517177c1 100644 --- a/services/gateway-service/internal/transport/http/resourceapi/resource_handler.go +++ b/services/gateway-service/internal/transport/http/resourceapi/resource_handler.go @@ -12,6 +12,7 @@ import ( walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/services/gateway-service/internal/auth" + "hyapp/services/gateway-service/internal/transport/http/resourceview" ) const resourceTypeEmojiPack = "emoji_pack" @@ -39,6 +40,7 @@ type resourceData struct { AssetURL string `json:"asset_url"` PreviewURL string `json:"preview_url"` AnimationURL string `json:"animation_url"` + Format string `json:"format,omitempty"` MetadataJSON string `json:"metadata_json"` SortOrder int32 `json:"sort_order"` CreatedAtMS int64 `json:"created_at_ms"` @@ -88,6 +90,12 @@ type resourceShopPurchaseRequestBody struct { CommandIDAlt string `json:"commandId"` } +type resourceEquipRequestBody struct { + EntitlementID string `json:"entitlement_id"` + CommandID string `json:"command_id"` + CommandIDAlt string `json:"commandId"` +} + type resourceShopPurchaseData struct { OrderID string `json:"order_id"` TransactionID string `json:"transaction_id"` @@ -475,18 +483,31 @@ func (h *Handler) equipMyResource(writer http.ResponseWriter, request *http.Requ httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } - var body struct { - EntitlementID string `json:"entitlement_id"` - } + var body resourceEquipRequestBody if request.Body != nil && request.Body != http.NoBody { if err := httpkit.DecodeJSON(request.Body, &body); err != nil && !errors.Is(err, io.EOF) { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidJSON, "invalid request body") return } } + commandID := strings.TrimSpace(body.CommandID) + if commandID == "" { + commandID = strings.TrimSpace(body.CommandIDAlt) + } + appCode := appcode.FromContext(request.Context()) + if commandID == "" { + if appCode == "fami" { + // Fami tiered VIP 是本次新契约,不能静默用每次都会变化的 request_id 伪装业务幂等。 + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") + return + } + // Lalu 等旧客户端没有业务 command_id;用本次 request_id 保持原有可调用性,但不承诺跨请求重放。 + commandID = "legacy_equip:" + httpkit.RequestIDFromContext(request.Context()) + } resp, err := h.walletClient.EquipUserResource(request.Context(), &walletv1.EquipUserResourceRequest{ RequestId: httpkit.RequestIDFromContext(request.Context()), - AppCode: appcode.FromContext(request.Context()), + CommandId: commandID, + AppCode: appCode, UserId: auth.UserIDFromContext(request.Context()), ResourceId: resourceID, EntitlementId: strings.TrimSpace(body.EntitlementID), @@ -550,6 +571,7 @@ func resourceFromProto(item *walletv1.Resource) resourceData { AssetURL: item.GetAssetUrl(), PreviewURL: item.GetPreviewUrl(), AnimationURL: item.GetAnimationUrl(), + Format: resourceview.FormatFromMetadata(item.GetMetadataJson()), MetadataJSON: item.GetMetadataJson(), SortOrder: item.GetSortOrder(), CreatedAtMS: item.GetCreatedAtMs(), diff --git a/services/gateway-service/internal/transport/http/resourceview/format.go b/services/gateway-service/internal/transport/http/resourceview/format.go new file mode 100644 index 00000000..6edc1a92 --- /dev/null +++ b/services/gateway-service/internal/transport/http/resourceview/format.go @@ -0,0 +1,24 @@ +// Package resourceview contains the stable client-facing projection rules for wallet resources. +package resourceview + +import ( + "encoding/json" + "strings" +) + +// FormatFromMetadata returns only an explicitly configured renderer identifier. format is the +// current contract and animation_format remains a legacy fallback; URL suffixes are intentionally +// never inspected because signed/CDN URLs are not a trustworthy media-format fact. +func FormatFromMetadata(raw string) string { + var metadata struct { + Format string `json:"format"` + AnimationFormat string `json:"animation_format"` + } + if json.Unmarshal([]byte(strings.TrimSpace(raw)), &metadata) != nil { + return "" + } + if format := strings.ToLower(strings.TrimSpace(metadata.Format)); format != "" { + return format + } + return strings.ToLower(strings.TrimSpace(metadata.AnimationFormat)) +} diff --git a/services/gateway-service/internal/transport/http/response_test.go b/services/gateway-service/internal/transport/http/response_test.go index 81748df7..4da70a8d 100644 --- a/services/gateway-service/internal/transport/http/response_test.go +++ b/services/gateway-service/internal/transport/http/response_test.go @@ -392,6 +392,7 @@ type fakeRoomQueryClient struct { lastFeeds *roomv1.ListRoomFeedsRequest lastMyRoom *roomv1.GetMyRoomRequest lastCurrent *roomv1.GetCurrentRoomRequest + lastVoicePresence *roomv1.BatchGetUserVoiceRoomPresencesRequest lastSnapshot *roomv1.GetRoomSnapshotRequest lastBackgrounds *roomv1.ListRoomBackgroundsRequest lastRocket *roomv1.GetRoomRocketRequest @@ -403,6 +404,7 @@ type fakeRoomQueryClient struct { feedsResp *roomv1.ListRoomsResponse myRoomResp *roomv1.GetMyRoomResponse currentResp *roomv1.GetCurrentRoomResponse + voicePresenceResp *roomv1.BatchGetUserVoiceRoomPresencesResponse snapshotResp *roomv1.GetRoomSnapshotResponse backgroundsResp *roomv1.ListRoomBackgroundsResponse rocketResp *roomv1.GetRoomRocketResponse @@ -414,6 +416,7 @@ type fakeRoomQueryClient struct { feedsErr error myRoomErr error currentErr error + voicePresenceErr error snapshotErr error backgroundsErr error rocketErr error @@ -1390,6 +1393,17 @@ func (f *fakeRoomQueryClient) GetCurrentRoom(_ context.Context, req *roomv1.GetC return &roomv1.GetCurrentRoomResponse{}, nil } +func (f *fakeRoomQueryClient) BatchGetUserVoiceRoomPresences(_ context.Context, req *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) { + f.lastVoicePresence = req + if f.voicePresenceErr != nil { + return nil, f.voicePresenceErr + } + if f.voicePresenceResp != nil { + return f.voicePresenceResp, nil + } + return &roomv1.BatchGetUserVoiceRoomPresencesResponse{}, nil +} + func (f *fakeRoomQueryClient) GetRoomSnapshot(_ context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) { f.lastSnapshot = req if f.snapshotErr != nil { @@ -4811,6 +4825,87 @@ func TestGetCurrentRoomUsesAuthenticatedUser(t *testing.T) { } } +func TestBatchUserVoiceRoomPresencesUsesAuthenticatedViewerAndStableJSON(t *testing.T) { + queryClient := &fakeRoomQueryClient{voicePresenceResp: &roomv1.BatchGetUserVoiceRoomPresencesResponse{ + Items: []*roomv1.UserVoiceRoomPresence{ + { + UserId: 1002, + State: "on_mic", + SeatNo: 3, + Joinable: true, + ObservedAtMs: 1_784_265_600_000, + Room: &roomv1.UserVoiceRoomPresenceRoom{ + RoomId: "room-profile-live", + RoomShortId: "886688", + OwnerUserId: 1001, + HostUserId: 1001, + Title: "Live Room", + CoverUrl: "https://cdn.example.com/room.png", + BackgroundUrl: "https://cdn.example.com/background.png", + Mode: "voice", + OnlineCount: 56, + SeatCount: 10, + Locked: true, + }, + }, + {UserId: 1003, State: "not_on_mic", ObservedAtMs: 1_784_265_600_000}, + }, + ServerTimeMs: 1_784_265_600_123, + }} + handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) + handler.SetRoomQueryClient(queryClient) + router := handler.Routes(auth.NewVerifier("secret")) + request := httptest.NewRequest(http.MethodGet, "/api/v1/users/voice-room-presence:batch?user_ids=1002,1003,1002", nil) + request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) + request.Header.Set("X-Request-ID", "req-profile-live") + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, request) + + if recorder.Code != http.StatusOK { + t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) + } + if queryClient.lastVoicePresence == nil || queryClient.lastVoicePresence.GetViewerUserId() != 42 { + t.Fatalf("voice presence query must use authenticated viewer: %+v", queryClient.lastVoicePresence) + } + if got := queryClient.lastVoicePresence.GetUserIds(); len(got) != 2 || got[0] != 1002 || got[1] != 1003 { + t.Fatalf("voice presence query must preserve first-seen deduplicated ids: %v", got) + } + if queryClient.lastVoicePresence.GetMeta().GetRequestId() != assertGeneratedRequestID(t, recorder, "req-profile-live") { + t.Fatalf("voice presence query must propagate generated request id: %+v", queryClient.lastVoicePresence.GetMeta()) + } + + var response httpkit.ResponseEnvelope + if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { + t.Fatalf("decode voice presence response failed: %v", err) + } + data := response.Data.(map[string]any) + items := data["items"].([]any) + onMic := items[0].(map[string]any) + room := onMic["room"].(map[string]any) + if onMic["user_id"] != "1002" || onMic["state"] != "on_mic" || onMic["joinable"] != true || room["room_id"] != "room-profile-live" || room["rtc_room_id"] != "room-profile-live" || room["locked"] != true { + t.Fatalf("on-mic response contract mismatch: %+v", onMic) + } + notOnMic := items[1].(map[string]any) + if notOnMic["state"] != "not_on_mic" || notOnMic["joinable"] != false || notOnMic["room"] != nil { + t.Fatalf("not-on-mic response must keep false and null fields: %+v", notOnMic) + } +} + +func TestBatchUserVoiceRoomPresencesRejectsInvalidIDs(t *testing.T) { + handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) + handler.SetRoomQueryClient(&fakeRoomQueryClient{}) + router := handler.Routes(auth.NewVerifier("secret")) + request := httptest.NewRequest(http.MethodGet, "/api/v1/users/voice-room-presence:batch?user_ids=1002,,1003", nil) + request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) + request.Header.Set("X-Request-ID", "req-profile-live-invalid") + recorder := httptest.NewRecorder() + + router.ServeHTTP(recorder, request) + + assertEnvelope(t, recorder, http.StatusBadRequest, "INVALID_USER_IDS", "req-profile-live-invalid") +} + func TestGetRoomSnapshotUsesAuthenticatedUser(t *testing.T) { queryClient := &fakeRoomQueryClient{snapshotResp: &roomv1.GetRoomSnapshotResponse{ Room: &roomv1.RoomSnapshot{ @@ -11029,6 +11124,7 @@ func TestProfileGateRejectsIncompleteUsersForRoomIMRTCPaidCapabilities(t *testin {name: "message_delete", method: http.MethodDelete, path: "/api/v1/messages/msg-1"}, {name: "direct_gift", method: http.MethodPost, path: "/api/v1/messages/direct-gifts/send", body: `{"command_id":"cmd-direct-gift","target_user_id":900002,"gift_id":"rose","gift_count":1}`}, {name: "profiles_batch", method: http.MethodGet, path: "/api/v1/users/profiles:batch?user_ids=42"}, + {name: "voice_room_presence_batch", method: http.MethodGet, path: "/api/v1/users/voice-room-presence:batch?user_ids=43"}, } for _, test := range tests { diff --git a/services/gateway-service/internal/transport/http/roomapi/handler.go b/services/gateway-service/internal/transport/http/roomapi/handler.go index 5e6034c1..2321900f 100644 --- a/services/gateway-service/internal/transport/http/roomapi/handler.go +++ b/services/gateway-service/internal/transport/http/roomapi/handler.go @@ -1,6 +1,8 @@ package roomapi import ( + "context" + "io" "net/http" "time" @@ -11,12 +13,25 @@ import ( "hyapp/services/gateway-service/internal/transport/http/httproutes" ) +// ObjectUploader 是房间专用媒体上传所需的最小 COS 能力。 +type ObjectUploader interface { + PutObject(ctx context.Context, key string, reader io.Reader, sizeBytes int64, contentType string) (string, error) +} + +type objectURLVerifier interface { + OwnsObjectURL(objectURL string, objectKey string) bool +} + // Handler owns room-facing HTTP endpoints in gateway. // It keeps gateway as protocol adapter only: room state still belongs to room-service. type Handler struct { roomClient client.RoomClient roomGuardClient client.RoomGuardClient roomQueryClient client.RoomQueryClient + roomVIPClient client.RoomVIPFeatureClient + roomMediaGuard client.RoomMediaGuardClient + objectUploader ObjectUploader + objectURLVerifier objectURLVerifier userProfileClient client.UserProfileClient userCountryClient client.UserCountryQueryClient userRegionClient client.UserRegionClient @@ -62,6 +77,7 @@ type Config struct { GiftMaxCount int GiftMaxUnits int RTCTokenConfig tencentrtc.TokenConfig + ObjectUploader ObjectUploader } func New(config Config) *Handler { @@ -82,10 +98,17 @@ func New(config Config) *Handler { giftMaxUnits = 5_000 } comboReader, _ := any(config.AppConfigReader).(giftComboConfigReader) + roomVIPClient, _ := any(config.RoomClient).(client.RoomVIPFeatureClient) + roomMediaGuard, _ := any(config.RoomGuardClient).(client.RoomMediaGuardClient) + urlVerifier, _ := any(config.ObjectUploader).(objectURLVerifier) return &Handler{ roomClient: config.RoomClient, roomGuardClient: config.RoomGuardClient, roomQueryClient: config.RoomQueryClient, + roomVIPClient: roomVIPClient, + roomMediaGuard: roomMediaGuard, + objectUploader: config.ObjectUploader, + objectURLVerifier: urlVerifier, userProfileClient: config.UserProfileClient, userCountryClient: config.UserCountryClient, userRegionClient: config.UserRegionClient, @@ -131,6 +154,10 @@ func (h *Handler) RoomHandlers() httproutes.RoomHandlers { SaveRoomBackground: h.saveRoomBackground, ListRoomBackgrounds: h.listRoomBackgrounds, SetRoomBackground: h.setRoomBackground, + UploadRoomBackground: h.uploadRoomBackground, + UploadRoomImage: h.uploadRoomImage, + ApplyRoomDecoration: h.applyRoomDecoration, + SendRoomImage: h.sendRoomImage, JoinRoom: h.joinRoom, RoomHeartbeat: h.roomHeartbeat, LeaveRoom: h.leaveRoom, @@ -161,3 +188,7 @@ func (h *Handler) IssueTencentRTCToken(writer http.ResponseWriter, request *http func (h *Handler) BatchRoomDisplayProfiles(writer http.ResponseWriter, request *http.Request) { h.batchRoomDisplayProfiles(writer, request) } + +func (h *Handler) BatchUserVoiceRoomPresences(writer http.ResponseWriter, request *http.Request) { + h.batchUserVoiceRoomPresences(writer, request) +} diff --git a/services/gateway-service/internal/transport/http/roomapi/room_handler.go b/services/gateway-service/internal/transport/http/roomapi/room_handler.go index 3a18ef80..6a7a8633 100644 --- a/services/gateway-service/internal/transport/http/roomapi/room_handler.go +++ b/services/gateway-service/internal/transport/http/roomapi/room_handler.go @@ -17,6 +17,7 @@ import ( "hyapp/pkg/roomid" "hyapp/pkg/xerr" "hyapp/services/gateway-service/internal/auth" + "hyapp/services/gateway-service/internal/transport/http/resourceview" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -1233,24 +1234,44 @@ func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Re // saveRoomBackground 保存房主上传后的背景图 URL,素材归 room-service 按房间维度管理。 func (h *Handler) saveRoomBackground(writer http.ResponseWriter, request *http.Request) { var body struct { - RoomID string `json:"room_id"` - ImageURL string `json:"image_url"` + RoomID string `json:"room_id"` + CommandID string `json:"command_id"` + ImageURL string `json:"image_url"` + Media *roomMediaData `json:"media"` } if !httpkit.Decode(writer, request, &body) { return } body.RoomID = strings.TrimSpace(body.RoomID) + body.CommandID = strings.TrimSpace(body.CommandID) body.ImageURL = strings.TrimSpace(body.ImageURL) + if body.Media != nil && body.ImageURL == "" { + body.ImageURL = strings.TrimSpace(body.Media.URL) + } + if body.Media != nil && body.ImageURL != strings.TrimSpace(body.Media.URL) { + // 新版请求的 image_url 只是 media.url 的兼容镜像;两者不一致时拒绝,避免调用方误以为保存了另一张图。 + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "image_url does not match media.url") + return + } if !roomid.ValidStringID(body.RoomID) || body.ImageURL == "" { httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } + var media *roomv1.RoomMedia + if body.Media != nil { + if h.objectURLVerifier == nil || !h.objectURLVerifier.OwnsObjectURL(body.Media.URL, body.Media.ObjectKey) { + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid room background media") + return + } + media = body.Media.proto() + } resp, err := h.roomClient.SaveRoomBackground(request.Context(), &roomv1.SaveRoomBackgroundRequest{ - Meta: httpkit.RoomMeta(request, body.RoomID, ""), + Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID), RoomId: body.RoomID, ImageUrl: body.ImageURL, + Media: media, }) httpkit.Write(writer, request, saveRoomBackgroundDataFromProto(resp), err) } @@ -2824,6 +2845,11 @@ func roomAppearanceResourceFromProto(item *walletv1.Resource, entitlementID stri "entitlement_id": entitlementID, "expires_at_ms": expiresAtMS, } + if format := resourceview.FormatFromMetadata(item.GetMetadataJson()); format != "" { + // 房间首屏/profile 是其他用户渲染麦位动效的权威快照,因此必须投影显式格式; + // 缺省时不猜 animation_url 后缀,避免 CDN 地址变化导致客户端选择错误 renderer。 + resource["format"] = format + } if item.GetResourceType() == "badge" { badgeForm, badgeKind, levelTrack := roomBadgeMetadataFields(item.GetMetadataJson()) resource["badge_form"] = badgeForm diff --git a/services/gateway-service/internal/transport/http/roomapi/room_media_handler.go b/services/gateway-service/internal/transport/http/roomapi/room_media_handler.go new file mode 100644 index 00000000..08030d06 --- /dev/null +++ b/services/gateway-service/internal/transport/http/roomapi/room_media_handler.go @@ -0,0 +1,564 @@ +package roomapi + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "encoding/hex" + "fmt" + "image/jpeg" + "image/png" + "io" + "mime" + "net/http" + "path/filepath" + "strings" + + roomv1 "hyapp.local/api/proto/room/v1" + "hyapp/pkg/giftlimits" + "hyapp/pkg/roomid" + "hyapp/services/gateway-service/internal/transport/http/httpkit" +) + +const ( + roomBackgroundUploadMaxBytes = int64(12 << 20) + roomImageUploadMaxBytes = int64(8 << 20) + roomUploadEnvelopeOverhead = int64(1 << 20) + roomUploadMaxFrames = int32(120) + roomUploadMaxDurationMS = int64(15_000) +) + +type roomMediaData struct { + URL string `json:"url"` + ObjectKey string `json:"object_key"` + ContentType string `json:"content_type"` + Format string `json:"format"` + SizeBytes int64 `json:"size_bytes"` + Width int32 `json:"width"` + Height int32 `json:"height"` + Animated bool `json:"animated"` + FrameCount int32 `json:"frame_count"` + DurationMS int64 `json:"duration_ms"` + PosterURL string `json:"poster_url,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` + SHA256 string `json:"sha256"` + Status string `json:"status"` +} + +type roomMediaUploadData struct { + Purpose string `json:"purpose"` + RoomID string `json:"room_id"` + CommandID string `json:"command_id"` + RoomVersion int64 `json:"room_version"` + Media roomMediaData `json:"media"` + ServerTimeMS int64 `json:"server_time_ms"` +} + +func (data roomMediaData) proto() *roomv1.RoomMedia { + return &roomv1.RoomMedia{ + Url: strings.TrimSpace(data.URL), ObjectKey: strings.TrimLeft(strings.TrimSpace(data.ObjectKey), "/"), + ContentType: strings.ToLower(strings.TrimSpace(data.ContentType)), Format: strings.ToLower(strings.TrimSpace(data.Format)), + SizeBytes: data.SizeBytes, Width: data.Width, Height: data.Height, Animated: data.Animated, + FrameCount: data.FrameCount, DurationMs: data.DurationMS, PosterUrl: strings.TrimSpace(data.PosterURL), + ThumbnailUrl: strings.TrimSpace(data.ThumbnailURL), Sha256: strings.ToLower(strings.TrimSpace(data.SHA256)), + Status: strings.ToLower(strings.TrimSpace(data.Status)), + } +} + +func roomMediaDataFromProto(media *roomv1.RoomMedia) *roomMediaData { + if media == nil || (strings.TrimSpace(media.GetUrl()) == "" && strings.TrimSpace(media.GetObjectKey()) == "") { + return nil + } + return &roomMediaData{ + URL: media.GetUrl(), ObjectKey: media.GetObjectKey(), ContentType: media.GetContentType(), Format: media.GetFormat(), + SizeBytes: media.GetSizeBytes(), Width: media.GetWidth(), Height: media.GetHeight(), Animated: media.GetAnimated(), + FrameCount: media.GetFrameCount(), DurationMS: media.GetDurationMs(), PosterURL: media.GetPosterUrl(), + ThumbnailURL: media.GetThumbnailUrl(), SHA256: media.GetSha256(), Status: media.GetStatus(), + } +} + +func (h *Handler) uploadRoomBackground(writer http.ResponseWriter, request *http.Request) { + h.uploadRoomMedia(writer, request, "room_background", roomBackgroundUploadMaxBytes) +} + +func (h *Handler) uploadRoomImage(writer http.ResponseWriter, request *http.Request) { + h.uploadRoomMedia(writer, request, "room_image", roomImageUploadMaxBytes) +} + +func (h *Handler) uploadRoomMedia(writer http.ResponseWriter, request *http.Request, purpose string, maxBytes int64) { + roomID := strings.TrimSpace(request.PathValue("room_id")) + if !roomid.ValidStringID(roomID) { + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "room_id is invalid") + return + } + if h.objectUploader == nil || h.objectURLVerifier == nil || h.roomMediaGuard == nil { + httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "room media upload is unavailable") + return + } + request.Body = http.MaxBytesReader(writer, request.Body, maxBytes+roomUploadEnvelopeOverhead) + if err := request.ParseMultipartForm(maxBytes + roomUploadEnvelopeOverhead); err != nil { + httpkit.WriteError(writer, request, http.StatusRequestEntityTooLarge, "FILE_TOO_LARGE", "file exceeds upload limit") + return + } + if request.MultipartForm != nil { + defer request.MultipartForm.RemoveAll() + } + commandID := strings.TrimSpace(request.FormValue("command_id")) + if !giftlimits.ValidCommandID(commandID) { + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "command_id is invalid") + return + } + file, header, err := request.FormFile("file") + if err != nil { + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "file is required") + return + } + defer file.Close() + content, err := io.ReadAll(io.LimitReader(file, maxBytes+1)) + if err != nil || int64(len(content)) == 0 { + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "file is invalid") + return + } + if int64(len(content)) > maxBytes { + httpkit.WriteError(writer, request, http.StatusRequestEntityTooLarge, "FILE_TOO_LARGE", "file exceeds upload limit") + return + } + media, _, err := inspectRoomImage(content, header.Filename, header.Header.Get("Content-Type")) + if err != nil { + httpkit.WriteError(writer, request, http.StatusUnsupportedMediaType, "UNSUPPORTED_MEDIA_TYPE", err.Error()) + return + } + if err := validateRoomUploadDimensions(purpose, media); err != nil { + httpkit.WriteError(writer, request, http.StatusBadRequest, "INVALID_MEDIA_DIMENSIONS", err.Error()) + return + } + authorization, err := h.roomMediaGuard.AuthorizeRoomMediaUpload(request.Context(), &roomv1.AuthorizeRoomMediaUploadRequest{ + Meta: httpkit.RoomMeta(request, roomID, commandID), Purpose: purpose, Media: media.proto(), + }) + if err != nil { + httpkit.WriteRPCError(writer, request, err) + return + } + if authorization == nil || !authorization.GetAllowed() || strings.TrimSpace(authorization.GetObjectKeyPrefix()) == "" || + strings.TrimSpace(authorization.GetObjectKey()) == "" { + httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "room media upload denied") + return + } + objectKey := strings.TrimLeft(strings.TrimSpace(authorization.GetObjectKey()), "/") + objectURL, err := h.objectUploader.PutObject(request.Context(), objectKey, bytes.NewReader(content), int64(len(content)), media.ContentType) + if err != nil { + httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "object upload failed") + return + } + if !h.objectURLVerifier.OwnsObjectURL(objectURL, objectKey) { + // 上传成功但返回地址不属于配置 access_url 时拒绝下发,避免把不可信 URL 固化进房间状态。 + httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "object upload url verification failed") + return + } + media.URL, media.ObjectKey, media.Status = objectURL, objectKey, "active" + completed, err := h.roomMediaGuard.CompleteRoomMediaUpload(request.Context(), &roomv1.CompleteRoomMediaUploadRequest{ + Meta: httpkit.RoomMeta(request, roomID, commandID), Purpose: purpose, Media: media.proto(), + }) + if err != nil { + httpkit.WriteRPCError(writer, request, err) + return + } + if completed == nil || !completed.GetActive() { + httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "room media upload completion failed") + return + } + completedMedia := roomMediaDataFromProto(completed.GetMedia()) + if completedMedia == nil { + httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "room media upload completion failed") + return + } + media = *completedMedia + httpkit.WriteOK(writer, request, roomMediaUploadData{ + Purpose: purpose, RoomID: roomID, CommandID: commandID, RoomVersion: authorization.GetRoomVersion(), + Media: media, ServerTimeMS: completed.GetServerTimeMs(), + }) +} + +func inspectRoomImage(content []byte, filename string, partContentType string) (roomMediaData, string, error) { + var data roomMediaData + var extension string + switch { + case len(content) >= 3 && content[0] == 0xff && content[1] == 0xd8 && content[2] == 0xff: + cfg, err := jpeg.DecodeConfig(bytes.NewReader(content)) + if err != nil { + return data, "", fmt.Errorf("invalid jpeg") + } + data.Format, data.ContentType, data.Width, data.Height, data.FrameCount = "jpeg", "image/jpeg", int32(cfg.Width), int32(cfg.Height), 1 + extension = "jpg" + case len(content) >= 8 && bytes.Equal(content[:8], []byte{137, 80, 78, 71, 13, 10, 26, 10}): + if pngHasAnimationChunk(content) { + return data, "", fmt.Errorf("animated png is not supported") + } + cfg, err := png.DecodeConfig(bytes.NewReader(content)) + if err != nil { + return data, "", fmt.Errorf("invalid png") + } + data.Format, data.ContentType, data.Width, data.Height, data.FrameCount = "png", "image/png", int32(cfg.Width), int32(cfg.Height), 1 + extension = "png" + case len(content) >= 6 && (string(content[:6]) == "GIF87a" || string(content[:6]) == "GIF89a"): + width, height, frames, durationMS, err := inspectGIF(content) + if err != nil { + return data, "", err + } + data.Format, data.ContentType, data.Width, data.Height = "gif", "image/gif", width, height + data.FrameCount, data.Animated, data.DurationMS = frames, frames > 1, durationMS + if !data.Animated { + data.DurationMS = 0 + } + extension = "gif" + case len(content) >= 12 && string(content[:4]) == "RIFF" && string(content[8:12]) == "WEBP": + width, height, frames, durationMS, animated, err := inspectWebP(content) + if err != nil { + return data, "", err + } + data.Format, data.ContentType, data.Width, data.Height = "webp", "image/webp", width, height + data.FrameCount, data.DurationMS, data.Animated = frames, durationMS, animated + extension = "webp" + default: + return data, "", fmt.Errorf("only jpeg, png, gif and webp are supported") + } + declared, _, _ := mime.ParseMediaType(strings.TrimSpace(partContentType)) + if !strings.EqualFold(declared, data.ContentType) { + return roomMediaData{}, "", fmt.Errorf("file content_type does not match signature") + } + fileExtension := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), ".")) + validExtension := fileExtension == extension || (data.Format == "jpeg" && fileExtension == "jpeg") + if !validExtension { + return roomMediaData{}, "", fmt.Errorf("filename extension does not match signature") + } + sum := sha256.Sum256(content) + data.SizeBytes, data.SHA256, data.Status = int64(len(content)), hex.EncodeToString(sum[:]), "active" + return data, extension, nil +} + +func validateRoomUploadDimensions(purpose string, media roomMediaData) error { + if media.Width <= 0 || media.Height <= 0 || media.FrameCount <= 0 { + return fmt.Errorf("image dimensions are invalid") + } + if media.Animated && (media.FrameCount > roomUploadMaxFrames || media.DurationMS <= 0 || media.DurationMS > roomUploadMaxDurationMS) { + return fmt.Errorf("animation exceeds frame or duration limit") + } + pixels := int64(media.Width) * int64(media.Height) + if purpose == "room_background" { + if media.Width > 2160 || media.Height > 3840 || pixels > 8_294_400 { + return fmt.Errorf("room background dimensions exceed limit") + } + return nil + } + if media.Width > 4096 || media.Height > 4096 || pixels > 16_777_216 { + return fmt.Errorf("room image dimensions exceed limit") + } + return nil +} + +func pngHasAnimationChunk(content []byte) bool { + for offset := 8; offset+12 <= len(content); { + size := int(binary.BigEndian.Uint32(content[offset : offset+4])) + if size < 0 || offset+12+size > len(content) { + return false + } + if string(content[offset+4:offset+8]) == "acTL" { + return true + } + offset += 12 + size + } + return false +} + +func inspectGIF(content []byte) (int32, int32, int32, int64, error) { + if len(content) < 13 || (string(content[:6]) != "GIF87a" && string(content[:6]) != "GIF89a") { + return 0, 0, 0, 0, fmt.Errorf("invalid gif") + } + width := int32(binary.LittleEndian.Uint16(content[6:8])) + height := int32(binary.LittleEndian.Uint16(content[8:10])) + if width <= 0 || height <= 0 || width > 4096 || height > 4096 || int64(width)*int64(height) > 16_777_216 { + // 先检查逻辑画布,后续只扫描块边界,不按攻击者声明的尺寸分配像素缓冲区。 + return 0, 0, 0, 0, fmt.Errorf("gif dimensions exceed limit") + } + + offset := 13 + if content[10]&0x80 != 0 { + colorTableBytes := 3 * (1 << (int(content[10]&0x07) + 1)) + if offset+colorTableBytes > len(content) { + return 0, 0, 0, 0, fmt.Errorf("invalid gif color table") + } + offset += colorTableBytes + } + + var frames int32 + var durationMS int64 + var pendingDelayMS int64 + for offset < len(content) { + marker := content[offset] + offset++ + switch marker { + case 0x3b: // trailer + if frames <= 0 { + return 0, 0, 0, 0, fmt.Errorf("gif has no image frame") + } + if frames == 1 { + durationMS = 0 + } + return width, height, frames, durationMS, nil + case 0x21: // extension + if offset >= len(content) { + return 0, 0, 0, 0, fmt.Errorf("invalid gif extension") + } + label := content[offset] + offset++ + if label == 0xf9 { + // Graphic Control Extension 固定为 4 字节数据和 0 终止符;delay 单位是 1/100 秒。 + if offset+6 > len(content) || content[offset] != 4 || content[offset+5] != 0 { + return 0, 0, 0, 0, fmt.Errorf("invalid gif graphic control extension") + } + pendingDelayMS = int64(binary.LittleEndian.Uint16(content[offset+2:offset+4])) * 10 + offset += 6 + continue + } + var skipErr error + offset, _, skipErr = skipGIFSubBlocks(content, offset) + if skipErr != nil { + return 0, 0, 0, 0, skipErr + } + case 0x2c: // image descriptor + if offset+9 > len(content) { + return 0, 0, 0, 0, fmt.Errorf("invalid gif image descriptor") + } + left := int(binary.LittleEndian.Uint16(content[offset : offset+2])) + top := int(binary.LittleEndian.Uint16(content[offset+2 : offset+4])) + frameWidth := int(binary.LittleEndian.Uint16(content[offset+4 : offset+6])) + frameHeight := int(binary.LittleEndian.Uint16(content[offset+6 : offset+8])) + packed := content[offset+8] + if frameWidth <= 0 || frameHeight <= 0 || left+frameWidth > int(width) || top+frameHeight > int(height) { + return 0, 0, 0, 0, fmt.Errorf("invalid gif frame dimensions") + } + offset += 9 + if packed&0x80 != 0 { + colorTableBytes := 3 * (1 << (int(packed&0x07) + 1)) + if offset+colorTableBytes > len(content) { + return 0, 0, 0, 0, fmt.Errorf("invalid gif local color table") + } + offset += colorTableBytes + } + if offset >= len(content) || content[offset] < 2 || content[offset] > 8 { + return 0, 0, 0, 0, fmt.Errorf("invalid gif lzw code size") + } + offset++ + var hasImageData bool + var skipErr error + offset, hasImageData, skipErr = skipGIFSubBlocks(content, offset) + if skipErr != nil || !hasImageData { + return 0, 0, 0, 0, fmt.Errorf("invalid gif image data") + } + frames++ + durationMS += pendingDelayMS + pendingDelayMS = 0 + if frames > roomUploadMaxFrames || durationMS > roomUploadMaxDurationMS { + return 0, 0, 0, 0, fmt.Errorf("gif animation exceeds frame or duration limit") + } + default: + return 0, 0, 0, 0, fmt.Errorf("invalid gif block marker") + } + } + return 0, 0, 0, 0, fmt.Errorf("gif trailer is missing") +} + +func skipGIFSubBlocks(content []byte, offset int) (int, bool, error) { + hasData := false + for { + if offset >= len(content) { + return 0, false, fmt.Errorf("invalid gif sub-block") + } + blockSize := int(content[offset]) + offset++ + if blockSize == 0 { + return offset, hasData, nil + } + if offset+blockSize > len(content) { + return 0, false, fmt.Errorf("invalid gif sub-block") + } + hasData = true + offset += blockSize + } +} + +func inspectWebP(content []byte) (int32, int32, int32, int64, bool, error) { + if len(content) < 20 || string(content[:4]) != "RIFF" || string(content[8:12]) != "WEBP" { + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp header") + } + declaredSize := uint64(binary.LittleEndian.Uint32(content[4:8])) + 8 + if declaredSize != uint64(len(content)) { + // RIFF size 不匹配通常表示截断或尾部拼接;禁止只扫到某个看似合法的前缀就接受整个对象。 + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp riff size") + } + + var width, height int32 + var frames int32 + var durationMS int64 + var animatedFlag, hasVP8X, hasANIM, hasStaticImage bool + for offset := 12; offset < len(content); { + if offset+8 > len(content) { + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp trailing chunk") + } + chunkType := string(content[offset : offset+4]) + size := uint64(binary.LittleEndian.Uint32(content[offset+4 : offset+8])) + start := uint64(offset + 8) + end := start + size + paddedEnd := end + size%2 + if end < start || paddedEnd > uint64(len(content)) { + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp chunk") + } + if size%2 == 1 && content[end] != 0 { + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp chunk padding") + } + chunk := content[int(start):int(end)] + switch chunkType { + case "VP8X": + if hasVP8X || offset != 12 || len(chunk) != 10 || chunk[0]&0xc1 != 0 || chunk[1] != 0 || chunk[2] != 0 || chunk[3] != 0 { + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp header") + } + hasVP8X = true + animatedFlag = chunk[0]&0x02 != 0 + width = int32(readUint24LE(chunk[4:7])) + 1 + height = int32(readUint24LE(chunk[7:10])) + 1 + case "VP8 ", "VP8L": + if hasStaticImage { + return 0, 0, 0, 0, false, fmt.Errorf("webp has multiple top-level image payloads") + } + if !hasVP8X && offset != 12 { + return 0, 0, 0, 0, false, fmt.Errorf("invalid simple webp chunk order") + } + imageWidth, imageHeight, err := inspectWebPImagePayload(chunkType, chunk) + if err != nil { + return 0, 0, 0, 0, false, err + } + hasStaticImage = true + if width == 0 { + width, height = imageWidth, imageHeight + } else if imageWidth > width || imageHeight > height { + return 0, 0, 0, 0, false, fmt.Errorf("webp image exceeds canvas") + } + case "ANIM": + if !hasVP8X || !animatedFlag || hasANIM || len(chunk) != 6 { + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp animation header") + } + hasANIM = true + case "ANMF": + if !hasVP8X || !animatedFlag || !hasANIM || width <= 0 || height <= 0 || len(chunk) < 16 || chunk[15]&0xfc != 0 { + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp animation frame") + } + frameX := int64(readUint24LE(chunk[0:3])) * 2 + frameY := int64(readUint24LE(chunk[3:6])) * 2 + frameWidth := int32(readUint24LE(chunk[6:9])) + 1 + frameHeight := int32(readUint24LE(chunk[9:12])) + 1 + if frameWidth <= 0 || frameHeight <= 0 || frameX+int64(frameWidth) > int64(width) || frameY+int64(frameHeight) > int64(height) { + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp animation frame geometry") + } + if err := inspectWebPFramePayload(chunk[16:], frameWidth, frameHeight); err != nil { + return 0, 0, 0, 0, false, err + } + frames++ + durationMS += int64(readUint24LE(chunk[12:15])) + if frames > roomUploadMaxFrames || durationMS > roomUploadMaxDurationMS { + return 0, 0, 0, 0, false, fmt.Errorf("webp animation exceeds frame or duration limit") + } + } + offset = int(paddedEnd) + } + if width <= 0 || height <= 0 { + return 0, 0, 0, 0, false, fmt.Errorf("invalid webp dimensions") + } + if animatedFlag { + if !hasANIM || hasStaticImage || frames < 2 || durationMS <= 0 { + return 0, 0, 0, 0, false, fmt.Errorf("invalid animated webp") + } + return width, height, frames, durationMS, true, nil + } + if hasANIM || frames != 0 || !hasStaticImage { + return 0, 0, 0, 0, false, fmt.Errorf("invalid static webp") + } + return width, height, 1, 0, false, nil +} + +func inspectWebPImagePayload(chunkType string, chunk []byte) (int32, int32, error) { + switch chunkType { + case "VP8 ": + // 10 字节只覆盖 lossy key-frame header;至少再有一个压缩 payload 字节,防止伪造尺寸头通过。 + if len(chunk) <= 10 || chunk[0]&0x01 != 0 || !bytes.Equal(chunk[3:6], []byte{0x9d, 0x01, 0x2a}) { + return 0, 0, fmt.Errorf("invalid webp vp8 payload") + } + width := int32(binary.LittleEndian.Uint16(chunk[6:8]) & 0x3fff) + height := int32(binary.LittleEndian.Uint16(chunk[8:10]) & 0x3fff) + if width <= 0 || height <= 0 { + return 0, 0, fmt.Errorf("invalid webp vp8 dimensions") + } + return width, height, nil + case "VP8L": + // VP8L signature+尺寸头为 5 字节;version 位必须为 0,并要求存在实际压缩 payload。 + if len(chunk) <= 5 || chunk[0] != 0x2f { + return 0, 0, fmt.Errorf("invalid webp vp8l payload") + } + bits := binary.LittleEndian.Uint32(chunk[1:5]) + if bits>>29 != 0 { + return 0, 0, fmt.Errorf("invalid webp vp8l version") + } + return int32(bits&0x3fff) + 1, int32((bits>>14)&0x3fff) + 1, nil + default: + return 0, 0, fmt.Errorf("unsupported webp image payload") + } +} + +func inspectWebPFramePayload(content []byte, expectedWidth int32, expectedHeight int32) error { + if len(content) < 8 { + return fmt.Errorf("webp animation frame has no image payload") + } + foundImage := false + for offset := 0; offset < len(content); { + if offset+8 > len(content) { + return fmt.Errorf("invalid webp animation frame chunk") + } + chunkType := string(content[offset : offset+4]) + size := uint64(binary.LittleEndian.Uint32(content[offset+4 : offset+8])) + start := uint64(offset + 8) + end := start + size + paddedEnd := end + size%2 + if end < start || paddedEnd > uint64(len(content)) { + return fmt.Errorf("invalid webp animation frame chunk") + } + if size%2 == 1 && content[end] != 0 { + return fmt.Errorf("invalid webp animation frame padding") + } + chunk := content[int(start):int(end)] + switch chunkType { + case "ALPH": + if foundImage || len(chunk) == 0 { + return fmt.Errorf("invalid webp animation alpha payload") + } + case "VP8 ", "VP8L": + if foundImage { + return fmt.Errorf("webp animation frame has multiple image payloads") + } + width, height, err := inspectWebPImagePayload(chunkType, chunk) + if err != nil || width != expectedWidth || height != expectedHeight { + return fmt.Errorf("webp animation image dimensions mismatch") + } + foundImage = true + default: + return fmt.Errorf("unsupported webp animation frame chunk") + } + offset = int(paddedEnd) + } + if !foundImage { + return fmt.Errorf("webp animation frame has no image payload") + } + return nil +} + +func readUint24LE(value []byte) uint32 { + return uint32(value[0]) | uint32(value[1])<<8 | uint32(value[2])<<16 +} diff --git a/services/gateway-service/internal/transport/http/roomapi/room_view.go b/services/gateway-service/internal/transport/http/roomapi/room_view.go index c96f3553..003b8c5e 100644 --- a/services/gateway-service/internal/transport/http/roomapi/room_view.go +++ b/services/gateway-service/internal/transport/http/roomapi/room_view.go @@ -35,23 +35,25 @@ type myRoomData struct { } type roomListItemData struct { - RoomID string `json:"room_id"` - IMGroupID string `json:"im_group_id"` - OwnerUserID string `json:"owner_user_id,omitempty"` - Title string `json:"title,omitempty"` - CoverURL string `json:"cover_url,omitempty"` - Mode string `json:"mode,omitempty"` - Status string `json:"status,omitempty"` - Locked bool `json:"locked"` - Heat int64 `json:"heat"` - OnlineCount int32 `json:"online_count"` - SeatCount int32 `json:"seat_count"` - OccupiedSeatCount int32 `json:"occupied_seat_count"` - VisibleRegionID int64 `json:"visible_region_id,omitempty"` - AppCode string `json:"app_code,omitempty"` - RoomShortID string `json:"room_short_id,omitempty"` - CountryCode string `json:"country_code,omitempty"` - CountryFlag string `json:"country_flag,omitempty"` + RoomID string `json:"room_id"` + IMGroupID string `json:"im_group_id"` + OwnerUserID string `json:"owner_user_id,omitempty"` + Title string `json:"title,omitempty"` + CoverURL string `json:"cover_url,omitempty"` + Mode string `json:"mode,omitempty"` + Status string `json:"status,omitempty"` + Locked bool `json:"locked"` + Heat int64 `json:"heat"` + OnlineCount int32 `json:"online_count"` + SeatCount int32 `json:"seat_count"` + OccupiedSeatCount int32 `json:"occupied_seat_count"` + VisibleRegionID int64 `json:"visible_region_id,omitempty"` + AppCode string `json:"app_code,omitempty"` + RoomShortID string `json:"room_short_id,omitempty"` + CountryCode string `json:"country_code,omitempty"` + CountryFlag string `json:"country_flag,omitempty"` + RoomBorder *roomDecorationData `json:"room_border,omitempty"` + RoomNameStyle *roomDecorationData `json:"room_name_style,omitempty"` } type createRoomData struct { @@ -378,12 +380,14 @@ type updateRoomProfileData struct { } type roomBackgroundData struct { - BackgroundID int64 `json:"background_id"` - RoomID string `json:"room_id"` - ImageURL string `json:"image_url"` - CreatedByUserID string `json:"created_by_user_id,omitempty"` - CreatedAtMS int64 `json:"created_at_ms"` - UpdatedAtMS int64 `json:"updated_at_ms"` + BackgroundID int64 `json:"background_id"` + RoomID string `json:"room_id"` + ImageURL string `json:"image_url"` + CreatedByUserID string `json:"created_by_user_id,omitempty"` + CreatedAtMS int64 `json:"created_at_ms"` + UpdatedAtMS int64 `json:"updated_at_ms"` + CommandID string `json:"command_id,omitempty"` + Media *roomMediaData `json:"media,omitempty"` } type saveRoomBackgroundData struct { @@ -394,6 +398,7 @@ type saveRoomBackgroundData struct { type listRoomBackgroundsData struct { Backgrounds []roomBackgroundData `json:"backgrounds"` SelectedBackgroundURL string `json:"selected_background_url,omitempty"` + SelectedBackground *roomBackgroundData `json:"selected_background,omitempty"` ServerTimeMS int64 `json:"server_time_ms"` } @@ -426,24 +431,27 @@ type roomCommandResultData struct { } type roomInitialData struct { - RoomID string `json:"room_id"` - IMGroupID string `json:"im_group_id"` - RoomShortID string `json:"room_short_id,omitempty"` - Title string `json:"title,omitempty"` - CoverURL string `json:"cover_url,omitempty"` - BackgroundURL string `json:"background_url,omitempty"` - Description string `json:"description,omitempty"` - OwnerUserID string `json:"owner_user_id,omitempty"` - Mode string `json:"mode,omitempty"` - Status string `json:"status,omitempty"` - ChatEnabled bool `json:"chat_enabled"` - Locked bool `json:"locked"` - Heat int64 `json:"heat"` - OnlineCount int32 `json:"online_count"` - SeatCount int32 `json:"seat_count"` - OccupiedSeatCount int32 `json:"occupied_seat_count"` - VisibleRegionID int64 `json:"visible_region_id,omitempty"` - Version int64 `json:"version"` + RoomID string `json:"room_id"` + IMGroupID string `json:"im_group_id"` + RoomShortID string `json:"room_short_id,omitempty"` + Title string `json:"title,omitempty"` + CoverURL string `json:"cover_url,omitempty"` + BackgroundURL string `json:"background_url,omitempty"` + Description string `json:"description,omitempty"` + OwnerUserID string `json:"owner_user_id,omitempty"` + Mode string `json:"mode,omitempty"` + Status string `json:"status,omitempty"` + ChatEnabled bool `json:"chat_enabled"` + Locked bool `json:"locked"` + Heat int64 `json:"heat"` + OnlineCount int32 `json:"online_count"` + SeatCount int32 `json:"seat_count"` + OccupiedSeatCount int32 `json:"occupied_seat_count"` + VisibleRegionID int64 `json:"visible_region_id,omitempty"` + Version int64 `json:"version"` + ActiveBackground *roomBackgroundData `json:"active_background,omitempty"` + RoomBorder *roomDecorationData `json:"room_border,omitempty"` + RoomNameStyle *roomDecorationData `json:"room_name_style,omitempty"` } type roomViewerData struct { @@ -744,6 +752,8 @@ func roomListItemDataFromProto(room *roomv1.RoomListItem) roomListItemData { RoomShortID: room.GetRoomShortId(), CountryCode: room.GetCountryCode(), CountryFlag: roomProfileCountryFlag(room.GetCountryCode()), + RoomBorder: roomDecorationDataFromProto(room.GetRoomBorder()), + RoomNameStyle: roomDecorationDataFromProto(room.GetRoomNameStyle()), } } @@ -792,6 +802,7 @@ func listRoomBackgroundsDataFromProto(resp *roomv1.ListRoomBackgroundsResponse) return listRoomBackgroundsData{ Backgrounds: backgrounds, SelectedBackgroundURL: resp.GetSelectedBackgroundUrl(), + SelectedBackground: roomBackgroundDataPointerFromProto(resp.GetSelectedBackground()), ServerTimeMS: resp.GetServerTimeMs(), } } @@ -820,9 +831,19 @@ func roomBackgroundDataFromProto(background *roomv1.RoomBackgroundImage) roomBac CreatedByUserID: formatOptionalUserID(background.GetCreatedByUserId()), CreatedAtMS: background.GetCreatedAtMs(), UpdatedAtMS: background.GetUpdatedAtMs(), + CommandID: background.GetCommandId(), + Media: roomMediaDataFromProto(background.GetMedia()), } } +func roomBackgroundDataPointerFromProto(background *roomv1.RoomBackgroundImage) *roomBackgroundData { + if background == nil || background.GetBackgroundId() <= 0 { + return nil + } + data := roomBackgroundDataFromProto(background) + return &data +} + func roomSnapshotDataFromProto(resp *roomv1.GetRoomSnapshotResponse) roomSnapshotData { if resp == nil { return roomSnapshotData{} @@ -874,6 +895,9 @@ func roomInitialRoomDataFromSnapshot(snapshot *roomv1.RoomSnapshot, roomID strin OccupiedSeatCount: occupiedSeatCount, VisibleRegionID: snapshot.GetVisibleRegionId(), Version: snapshot.GetVersion(), + ActiveBackground: roomBackgroundDataPointerFromProto(snapshot.GetActiveBackground()), + RoomBorder: roomDecorationDataFromProto(snapshot.GetRoomBorder()), + RoomNameStyle: roomDecorationDataFromProto(snapshot.GetRoomNameStyle()), } } diff --git a/services/gateway-service/internal/transport/http/roomapi/room_vip_handler.go b/services/gateway-service/internal/transport/http/roomapi/room_vip_handler.go new file mode 100644 index 00000000..51225705 --- /dev/null +++ b/services/gateway-service/internal/transport/http/roomapi/room_vip_handler.go @@ -0,0 +1,134 @@ +package roomapi + +import ( + "net/http" + "strings" + + roomv1 "hyapp.local/api/proto/room/v1" + "hyapp/pkg/giftlimits" + "hyapp/pkg/roomid" + "hyapp/services/gateway-service/internal/transport/http/httpkit" +) + +type roomDecorationData struct { + ResourceID int64 `json:"resource_id"` + ResourceCode string `json:"resource_code"` + ResourceType string `json:"resource_type"` + Name string `json:"name"` + AssetURL string `json:"asset_url"` + PreviewURL string `json:"preview_url,omitempty"` + AnimationURL string `json:"animation_url,omitempty"` + Format string `json:"format,omitempty"` + MetadataJSON string `json:"metadata_json,omitempty"` + EntitlementID string `json:"entitlement_id"` + TextColors []string `json:"text_colors,omitempty"` + Stops []float64 `json:"stops,omitempty"` + AngleDegrees float64 `json:"angle_degrees,omitempty"` + ExpiresAtMS int64 `json:"expires_at_ms,omitempty"` +} + +type applyRoomDecorationData struct { + Result roomCommandResultData `json:"result"` + Room roomInitialData `json:"room"` + DecorationType string `json:"decoration_type"` + Resource *roomDecorationData `json:"resource,omitempty"` + ServerTimeMS int64 `json:"server_time_ms"` +} + +type sendRoomImageData struct { + Result roomCommandResultData `json:"result"` + Accepted bool `json:"accepted"` + MessageID string `json:"message_id"` + RoomID string `json:"room_id"` + SenderUserID string `json:"sender_user_id"` + ServerTimeMS int64 `json:"server_time_ms"` + Image *roomMediaData `json:"image,omitempty"` +} + +func (h *Handler) applyRoomDecoration(writer http.ResponseWriter, request *http.Request) { + if h.roomVIPClient == nil { + httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "room vip feature is unavailable") + return + } + roomID := strings.TrimSpace(request.PathValue("room_id")) + var body struct { + CommandID string `json:"command_id"` + DecorationType string `json:"decoration_type"` + ResourceID flexibleInt64 `json:"resource_id"` + EntitlementID string `json:"entitlement_id"` + } + if !httpkit.Decode(writer, request, &body) { + return + } + body.CommandID = strings.TrimSpace(body.CommandID) + body.DecorationType = strings.ToLower(strings.TrimSpace(body.DecorationType)) + body.EntitlementID = strings.TrimSpace(body.EntitlementID) + if !roomid.ValidStringID(roomID) || !giftlimits.ValidCommandID(body.CommandID) || int64(body.ResourceID) <= 0 { + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") + return + } + resp, err := h.roomVIPClient.ApplyRoomDecoration(request.Context(), &roomv1.ApplyRoomDecorationRequest{ + Meta: httpkit.RoomMeta(request, roomID, body.CommandID), DecorationType: body.DecorationType, + ResourceId: int64(body.ResourceID), EntitlementId: body.EntitlementID, + }) + httpkit.Write(writer, request, applyRoomDecorationDataFromProto(resp), err) +} + +func (h *Handler) sendRoomImage(writer http.ResponseWriter, request *http.Request) { + if h.roomVIPClient == nil || h.objectURLVerifier == nil { + httpkit.WriteError(writer, request, http.StatusServiceUnavailable, httpkit.CodeUpstreamError, "room image message is unavailable") + return + } + roomID := strings.TrimSpace(request.PathValue("room_id")) + var body struct { + CommandID string `json:"command_id"` + Image roomMediaData `json:"image"` + } + if !httpkit.Decode(writer, request, &body) { + return + } + body.CommandID = strings.TrimSpace(body.CommandID) + if !roomid.ValidStringID(roomID) || !giftlimits.ValidCommandID(body.CommandID) || + !h.objectURLVerifier.OwnsObjectURL(body.Image.URL, body.Image.ObjectKey) { + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid room image") + return + } + resp, err := h.roomVIPClient.SendRoomImage(request.Context(), &roomv1.SendRoomImageRequest{ + Meta: httpkit.RoomMeta(request, roomID, body.CommandID), Image: body.Image.proto(), + }) + httpkit.Write(writer, request, sendRoomImageDataFromProto(resp), err) +} + +func applyRoomDecorationDataFromProto(resp *roomv1.ApplyRoomDecorationResponse) applyRoomDecorationData { + if resp == nil { + return applyRoomDecorationData{} + } + return applyRoomDecorationData{ + Result: commandResultDataFromProto(resp.GetResult()), Room: roomInitialRoomDataFromSnapshot(resp.GetRoom(), resp.GetRoom().GetRoomId()), + DecorationType: resp.GetDecorationType(), Resource: roomDecorationDataFromProto(resp.GetResource()), ServerTimeMS: resp.GetServerTimeMs(), + } +} + +func sendRoomImageDataFromProto(resp *roomv1.SendRoomImageResponse) sendRoomImageData { + if resp == nil { + return sendRoomImageData{} + } + return sendRoomImageData{ + Result: commandResultDataFromProto(resp.GetResult()), Accepted: resp.GetAccepted(), MessageID: resp.GetMessageId(), + RoomID: resp.GetRoomId(), SenderUserID: formatOptionalUserID(resp.GetSenderUserId()), + ServerTimeMS: resp.GetServerTimeMs(), Image: roomMediaDataFromProto(resp.GetImage()), + } +} + +func roomDecorationDataFromProto(resource *roomv1.RoomDecorationResource) *roomDecorationData { + if resource == nil || resource.GetResourceId() <= 0 { + return nil + } + return &roomDecorationData{ + ResourceID: resource.GetResourceId(), ResourceCode: resource.GetResourceCode(), ResourceType: resource.GetResourceType(), + Name: resource.GetName(), AssetURL: resource.GetAssetUrl(), PreviewURL: resource.GetPreviewUrl(), AnimationURL: resource.GetAnimationUrl(), + Format: resource.GetFormat(), MetadataJSON: resource.GetMetadataJson(), EntitlementID: resource.GetEntitlementId(), + TextColors: append([]string(nil), resource.GetTextColors()...), Stops: append([]float64(nil), resource.GetStops()...), + AngleDegrees: resource.GetAngleDegrees(), ExpiresAtMS: resource.GetExpiresAtMs(), + } +} diff --git a/services/gateway-service/internal/transport/http/roomapi/user_voice_room_presence.go b/services/gateway-service/internal/transport/http/roomapi/user_voice_room_presence.go new file mode 100644 index 00000000..8ca627ac --- /dev/null +++ b/services/gateway-service/internal/transport/http/roomapi/user_voice_room_presence.go @@ -0,0 +1,146 @@ +package roomapi + +import ( + "context" + "net/http" + "strconv" + "strings" + + roomv1 "hyapp.local/api/proto/room/v1" + "hyapp/services/gateway-service/internal/auth" + "hyapp/services/gateway-service/internal/transport/http/httpkit" +) + +const ( + maxUserVoiceRoomPresenceBatchSize = 100 + codeInvalidUserIDs = "INVALID_USER_IDS" + codeVoiceRoomPresenceUnavailable = "VOICE_ROOM_PRESENCE_UNAVAILABLE" +) + +// userVoiceRoomPresenceQueryClient 是个人资料页独占的窄查询能力。 +// 生产 grpcRoomQueryClient 实现该接口;其他只使用房间列表的调用方无需为新 RPC 扩大 mock 面。 +type userVoiceRoomPresenceQueryClient interface { + BatchGetUserVoiceRoomPresences(context.Context, *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) +} + +type userVoiceRoomPresenceBatchData struct { + Items []userVoiceRoomPresenceData `json:"items"` + ServerTimeMS int64 `json:"server_time_ms"` +} + +type userVoiceRoomPresenceData struct { + UserID string `json:"user_id"` + State string `json:"state"` + SeatNo int32 `json:"seat_no"` + Joinable bool `json:"joinable"` + ObservedAtMS int64 `json:"observed_at_ms"` + Room *userVoiceRoomPresenceRoomData `json:"room"` +} + +type userVoiceRoomPresenceRoomData struct { + RoomID string `json:"room_id"` + IMGroupID string `json:"im_group_id"` + RTCRoomID string `json:"rtc_room_id"` + RoomShortID string `json:"room_short_id"` + Title string `json:"title"` + OwnerUserID string `json:"owner_user_id"` + HostUserID string `json:"host_user_id"` + Mode string `json:"mode"` + CoverURL string `json:"cover_url"` + BackgroundURL string `json:"background_url"` + OnlineCount int32 `json:"online_count"` + SeatCount int32 `json:"seat_count"` + Locked bool `json:"locked"` +} + +// batchUserVoiceRoomPresences 为个人资料页返回当前登录用户可见的 Live 入口。 +// 客户端只提交目标 user_ids;viewer_user_id 和 app_code 必须来自已验签请求上下文。 +func (h *Handler) batchUserVoiceRoomPresences(writer http.ResponseWriter, request *http.Request) { + queryClient, ok := h.roomQueryClient.(userVoiceRoomPresenceQueryClient) + if !ok || queryClient == nil { + httpkit.WriteError(writer, request, http.StatusServiceUnavailable, codeVoiceRoomPresenceUnavailable, "voice room presence unavailable") + return + } + userIDs, ok := parseUserVoiceRoomPresenceUserIDs(request) + if !ok { + httpkit.WriteError(writer, request, http.StatusBadRequest, codeInvalidUserIDs, "invalid user ids") + return + } + + resp, err := queryClient.BatchGetUserVoiceRoomPresences(request.Context(), &roomv1.BatchGetUserVoiceRoomPresencesRequest{ + Meta: httpkit.RoomMeta(request, "", ""), + ViewerUserId: auth.UserIDFromContext(request.Context()), + UserIds: userIDs, + }) + if err != nil { + // 单个未上麦或不可见用户由 room-service 返回 not_on_mic;RPC 失败代表整批实时事实不可用,客户端必须隐藏 Live。 + httpkit.WriteError(writer, request, http.StatusServiceUnavailable, codeVoiceRoomPresenceUnavailable, "voice room presence unavailable") + return + } + httpkit.WriteOK(writer, request, userVoiceRoomPresenceBatchDataFromProto(resp)) +} + +func parseUserVoiceRoomPresenceUserIDs(request *http.Request) ([]int64, bool) { + rawValues, exists := request.URL.Query()["user_ids"] + if !exists || len(rawValues) == 0 { + return nil, false + } + seen := make(map[int64]struct{}) + result := make([]int64, 0, len(rawValues)) + for _, raw := range rawValues { + if strings.TrimSpace(raw) == "" { + return nil, false + } + for piece := range strings.SplitSeq(raw, ",") { + piece = strings.TrimSpace(piece) + userID, err := strconv.ParseInt(piece, 10, 64) + if piece == "" || err != nil || userID <= 0 { + return nil, false + } + if _, duplicate := seen[userID]; duplicate { + continue + } + seen[userID] = struct{}{} + result = append(result, userID) + if len(result) > maxUserVoiceRoomPresenceBatchSize { + return nil, false + } + } + } + return result, len(result) > 0 +} + +func userVoiceRoomPresenceBatchDataFromProto(resp *roomv1.BatchGetUserVoiceRoomPresencesResponse) userVoiceRoomPresenceBatchData { + if resp == nil { + return userVoiceRoomPresenceBatchData{Items: []userVoiceRoomPresenceData{}} + } + items := make([]userVoiceRoomPresenceData, 0, len(resp.GetItems())) + for _, item := range resp.GetItems() { + view := userVoiceRoomPresenceData{ + UserID: httpkit.UserIDString(item.GetUserId()), + State: item.GetState(), + SeatNo: item.GetSeatNo(), + Joinable: item.GetJoinable(), + ObservedAtMS: item.GetObservedAtMs(), + } + if room := item.GetRoom(); room != nil { + view.Room = &userVoiceRoomPresenceRoomData{ + RoomID: room.GetRoomId(), + IMGroupID: roomIMGroupID(room.GetRoomId()), + RTCRoomID: room.GetRoomId(), + RoomShortID: room.GetRoomShortId(), + Title: room.GetTitle(), + OwnerUserID: httpkit.UserIDString(room.GetOwnerUserId()), + HostUserID: httpkit.UserIDString(room.GetHostUserId()), + Mode: room.GetMode(), + CoverURL: room.GetCoverUrl(), + BackgroundURL: room.GetBackgroundUrl(), + OnlineCount: room.GetOnlineCount(), + SeatCount: room.GetSeatCount(), + Locked: room.GetLocked(), + } + } + items = append(items, view) + } + return userVoiceRoomPresenceBatchData{Items: items, ServerTimeMS: resp.GetServerTimeMs()} +} diff --git a/services/gateway-service/internal/transport/http/router.go b/services/gateway-service/internal/transport/http/router.go index 37f5f317..be195af9 100644 --- a/services/gateway-service/internal/transport/http/router.go +++ b/services/gateway-service/internal/transport/http/router.go @@ -41,6 +41,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler { GiftMaxTargets: h.giftMaxTargets, GiftMaxCount: h.giftMaxCount, GiftMaxUnits: h.giftMaxUnits, + ObjectUploader: h.objectUploader, RTCTokenConfig: tencentrtc.TokenConfig{ Enabled: h.tencentRTC.Enabled, SDKAppID: h.tencentRTC.SDKAppID, @@ -153,6 +154,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler { userHandlers := userAPI.UserHandlers() userHandlers.BatchUserProfiles = messageAPI.BatchUserProfiles userHandlers.BatchRoomDisplayProfiles = roomAPI.BatchRoomDisplayProfiles + userHandlers.BatchUserVoiceRoomPresences = roomAPI.BatchUserVoiceRoomPresences userHandlers.GetMyGiftWall = walletAPI.GetMyGiftWall userHandlers.ListMyResources = resourceAPI.ListMyResources userHandlers.EquipMyResource = resourceAPI.EquipMyResource diff --git a/services/gateway-service/internal/transport/http/userapi/appearance_handler.go b/services/gateway-service/internal/transport/http/userapi/appearance_handler.go index 45c96322..87b24c26 100644 --- a/services/gateway-service/internal/transport/http/userapi/appearance_handler.go +++ b/services/gateway-service/internal/transport/http/userapi/appearance_handler.go @@ -13,6 +13,7 @@ import ( "hyapp/pkg/appcode" "hyapp/services/gateway-service/internal/auth" "hyapp/services/gateway-service/internal/transport/http/httpkit" + "hyapp/services/gateway-service/internal/transport/http/resourceview" ) const ( @@ -383,6 +384,11 @@ func appearanceResourceFromProto(item *walletv1.Resource, entitlementID string, "entitlement_id": entitlementID, "expires_at_ms": expiresAtMS, } + if format := resourceview.FormatFromMetadata(item.GetMetadataJson()); format != "" { + // 佩戴后的 appearance 仍要携带与资源列表相同的显式 renderer;Flutter 不解析 URL 后缀, + // 缺少后台格式配置时保持字段省略,让客户端明确走“不支持”分支。 + resource["format"] = format + } if item.GetResourceType() == appearanceResourceBadge { badgeForm, badgeKind, levelTrack := appearanceBadgeMetadataFields(item.GetMetadataJson()) resource["badge_form"] = badgeForm diff --git a/services/gateway-service/internal/transport/http/userapi/social_handler.go b/services/gateway-service/internal/transport/http/userapi/social_handler.go index 6f0e9da0..144e4307 100644 --- a/services/gateway-service/internal/transport/http/userapi/social_handler.go +++ b/services/gateway-service/internal/transport/http/userapi/social_handler.go @@ -1,19 +1,26 @@ package userapi import ( - "hyapp/services/gateway-service/internal/transport/http/httpkit" "net/http" "strings" userv1 "hyapp.local/api/proto/user/v1" "hyapp/services/gateway-service/internal/auth" + "hyapp/services/gateway-service/internal/transport/http/httpkit" +) + +const ( + profileVisitorPageMax int32 = 100 + profileVisitorPageSizeMax int32 = 100 ) type profileVisitRecordData struct { - VisitorUserID string `json:"visitor_user_id"` - TargetUserID string `json:"target_user_id"` - VisitCount int64 `json:"visit_count"` - LastVisitedAtMS int64 `json:"last_visited_at_ms"` + VisitorUserID string `json:"visitor_user_id,omitempty"` + AnonymousVisitID string `json:"anonymous_visit_id,omitempty"` + VisitorIdentityHidden bool `json:"visitor_identity_hidden"` + TargetUserID string `json:"target_user_id"` + VisitCount int64 `json:"visit_count"` + LastVisitedAtMS int64 `json:"last_visited_at_ms"` } type followRecordData struct { @@ -47,6 +54,18 @@ func (h *Handler) listMyProfileVisitors(writer http.ResponseWriter, request *htt httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument") return } + if page > profileVisitorPageMax { + // 当前 page 协议需要从公开、匿名两个有序分支读取到目标 offset 后再合并;限制页码可把 + // 单次读取窗口硬限制在 10,000 条,避免客户端构造高页把 user-service 内存变成无界缓存。 + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "page must be less than or equal to 100") + return + } + if pageSize > profileVisitorPageSizeMax { + // 访客列表的页宽是对外契约;网关必须在进入 gRPC 前拒绝超限请求, + // 不依赖 user-service 静默截断,否则 Flutter 无法区分参数错误与服务端分页。 + httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "page_size must be less than or equal to 100") + return + } resp, err := h.userSocialClient.ListProfileVisitors(request.Context(), &userv1.ListProfileVisitorsRequest{ Meta: httpkit.UserMeta(request, ""), UserId: auth.UserIDFromContext(request.Context()), @@ -192,7 +211,14 @@ func (h *Handler) recordProfileVisit(writer http.ResponseWriter, request *http.R httpkit.WriteRPCError(writer, request, err) return } - data := map[string]any{"recorded": resp.GetRecorded(), "target_stats": overviewStatsFromProto(resp.GetTargetStats())} + data := map[string]any{ + "recorded": resp.GetRecorded(), + "target_stats_hidden": resp.GetTargetStatsHidden(), + } + if !resp.GetTargetStatsHidden() { + // 隐藏标志为 true 时不写 target_stats key;不能把 nil 转成零值 DTO,否则客户端会把伪造的 0 当成真实统计。 + data["target_stats"] = overviewStatsFromProto(resp.GetTargetStats()) + } if h.userProfileClient != nil { profileResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{ Meta: httpkit.UserMeta(request, ""), @@ -303,12 +329,19 @@ func profileVisitData(record *userv1.ProfileVisitRecord) profileVisitRecordData if record == nil { return profileVisitRecordData{} } - return profileVisitRecordData{ - VisitorUserID: userIDString(record.GetVisitorUserId()), - TargetUserID: userIDString(record.GetTargetUserId()), - VisitCount: record.GetVisitCount(), - LastVisitedAtMS: record.GetLastVisitedAtMs(), + data := profileVisitRecordData{ + VisitorIdentityHidden: record.GetVisitorIdentityHidden(), + TargetUserID: userIDString(record.GetTargetUserId()), + VisitCount: record.GetVisitCount(), + LastVisitedAtMS: record.GetLastVisitedAtMs(), } + if record.GetVisitorIdentityHidden() { + // 匿名记录只暴露随机展示 ID;visitor_user_id 使用 omitempty 完全省略,也不存在可继续拉取的 profile。 + data.AnonymousVisitID = record.GetAnonymousVisitId() + } else { + data.VisitorUserID = userIDString(record.GetVisitorUserId()) + } + return data } func followData(record *userv1.FollowRecord) followRecordData { diff --git a/services/gateway-service/internal/transport/http/walletapi/app_wallet_handler.go b/services/gateway-service/internal/transport/http/walletapi/app_wallet_handler.go index dfa330be..c0d10fad 100644 --- a/services/gateway-service/internal/transport/http/walletapi/app_wallet_handler.go +++ b/services/gateway-service/internal/transport/http/walletapi/app_wallet_handler.go @@ -230,6 +230,8 @@ type vipBenefitData struct { CreatedAtMS int64 `json:"created_at_ms"` UpdatedAtMS int64 `json:"updated_at_ms"` Presentation *vipBenefitPresentationData `json:"presentation"` + // Resource 只有在 wallet 已确认同 App、active 且类型匹配时才非空。 + Resource *resourceData `json:"resource,omitempty"` } type vipBenefitPresentationData struct { diff --git a/services/gateway-service/internal/transport/http/walletapi/view_shared.go b/services/gateway-service/internal/transport/http/walletapi/view_shared.go index 8d05b218..15b7b95e 100644 --- a/services/gateway-service/internal/transport/http/walletapi/view_shared.go +++ b/services/gateway-service/internal/transport/http/walletapi/view_shared.go @@ -1,6 +1,9 @@ package walletapi -import walletv1 "hyapp.local/api/proto/wallet/v1" +import ( + walletv1 "hyapp.local/api/proto/wallet/v1" + "hyapp/services/gateway-service/internal/transport/http/resourceview" +) type overviewVIPData struct { Level int32 `json:"level"` @@ -30,6 +33,7 @@ type resourceData struct { AssetURL string `json:"asset_url"` PreviewURL string `json:"preview_url"` AnimationURL string `json:"animation_url"` + Format string `json:"format,omitempty"` MetadataJSON string `json:"metadata_json"` SortOrder int32 `json:"sort_order"` CreatedAtMS int64 `json:"created_at_ms"` @@ -65,6 +69,7 @@ func resourceFromProto(item *walletv1.Resource) resourceData { AssetURL: item.GetAssetUrl(), PreviewURL: item.GetPreviewUrl(), AnimationURL: item.GetAnimationUrl(), + Format: resourceview.FormatFromMetadata(item.GetMetadataJson()), MetadataJSON: item.GetMetadataJson(), SortOrder: item.GetSortOrder(), CreatedAtMS: item.GetCreatedAtMs(), diff --git a/services/gateway-service/internal/transport/http/walletapi/vip_handler.go b/services/gateway-service/internal/transport/http/walletapi/vip_handler.go index b376d4f3..9d018556 100644 --- a/services/gateway-service/internal/transport/http/walletapi/vip_handler.go +++ b/services/gateway-service/internal/transport/http/walletapi/vip_handler.go @@ -233,7 +233,12 @@ func (h *Handler) triggerVIPOnlineNotice(writer http.ResponseWriter, request *ht // 客户端展示态可能滞后,执行前必须重新查 effective benefit;体验卡排除、过期和后台停用 // 都以 wallet-service 此刻的判定为准,不能仅比较 VIP9 等级。 statusCode, code, message := httpkit.MapReasonToHTTP(xerr.VIPBenefitRequired) - httpkit.WriteError(writer, request, statusCode, code, message) + httpkit.WriteErrorData(writer, request, statusCode, code, message, map[string]any{ + "benefit_code": vipOnlineNoticeBenefitCode, + "required_level": benefitResp.GetRequiredLevel(), + "current_effective_level": benefitResp.GetCurrentEffectiveLevel(), + "action": "send_online_global_notice", + }) return } equippedResp, err := h.walletClient.BatchGetUserEquippedResources(ctx, &walletv1.BatchGetUserEquippedResourcesRequest{ @@ -438,6 +443,7 @@ func vipBenefitsFromProto(items []*walletv1.VipBenefit) []vipBenefitData { CreatedAtMS: item.GetCreatedAtMs(), UpdatedAtMS: item.GetUpdatedAtMs(), Presentation: vipBenefitPresentationFromProto(item.GetPresentation()), + Resource: resourcePointerFromProto(item.GetResource()), }) } return result diff --git a/services/lucky-gift-service/internal/service/luckygift/service.go b/services/lucky-gift-service/internal/service/luckygift/service.go index 362e8c97..665cde74 100644 --- a/services/lucky-gift-service/internal/service/luckygift/service.go +++ b/services/lucky-gift-service/internal/service/luckygift/service.go @@ -68,6 +68,13 @@ type luckyGiftUserProfileRefresher interface { RefreshLuckyGiftUserProfiles(ctx context.Context, nowMS int64, batchSize int) (int, error) } +// luckyGiftRuleVersionRepository 把历史版本能力保持为生产 MySQL 仓储的可选扩展,避免为只服务 +// 单元测试的最小 Repository 伪实现扩散后台运维方法。 +type luckyGiftRuleVersionRepository interface { + ListLuckyGiftRuleConfigVersions(ctx context.Context, poolID string) ([]domain.RuleConfig, error) + RollbackLuckyGiftRuleConfig(ctx context.Context, poolID string, targetRuleVersion int64, operatorAdminID int64, nowMS int64) (domain.RuleConfig, error) +} + // WalletClient 是幸运礼物返奖唯一账务依赖;wallet-service 仍负责持久 outbox,activity 只做成功落库后的低延迟余额 IM。 type WalletClient interface { CreditLuckyGiftReward(ctx context.Context, req *walletv1.CreditLuckyGiftRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditLuckyGiftRewardResponse, error) @@ -787,6 +794,42 @@ func (s *Service) ListConfigs(ctx context.Context, appCode string) ([]domain.Rul return s.repository.ListLuckyGiftRuleConfigs(ctx, strings.TrimSpace(appCode)) } +func (s *Service) ListConfigVersions(ctx context.Context, poolID string) ([]domain.RuleConfig, error) { + if err := s.requireRepository(); err != nil { + return nil, err + } + versionRepository, ok := s.repository.(luckyGiftRuleVersionRepository) + if !ok { + return nil, xerr.New(xerr.Unavailable, "lucky gift rule version repository is unavailable") + } + poolID = normalizePoolID(poolID) + if poolID == "" { + return nil, xerr.New(xerr.InvalidArgument, "lucky gift pool id is required") + } + return versionRepository.ListLuckyGiftRuleConfigVersions(ctx, poolID) +} + +func (s *Service) RollbackConfig(ctx context.Context, poolID string, targetRuleVersion int64, operatorAdminID int64) (domain.RuleConfig, error) { + if err := s.requireRepository(); err != nil { + return domain.RuleConfig{}, err + } + versionRepository, ok := s.repository.(luckyGiftRuleVersionRepository) + if !ok { + return domain.RuleConfig{}, xerr.New(xerr.Unavailable, "lucky gift rule version repository is unavailable") + } + poolID = normalizePoolID(poolID) + if poolID == "" || targetRuleVersion <= 0 || operatorAdminID <= 0 { + return domain.RuleConfig{}, xerr.New(xerr.InvalidArgument, "lucky gift rollback request is incomplete") + } + return versionRepository.RollbackLuckyGiftRuleConfig( + ctx, + poolID, + targetRuleVersion, + operatorAdminID, + s.now().UTC().UnixMilli(), + ) +} + func (s *Service) ListDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error) { if err := s.requireRepository(); err != nil { return nil, 0, err diff --git a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_rule_config_repository.go b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_rule_config_repository.go index 2b9e77c4..a6d95b7a 100644 --- a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_rule_config_repository.go +++ b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_rule_config_repository.go @@ -251,6 +251,95 @@ func (r *Repository) ListLuckyGiftRuleConfigs(ctx context.Context, appCode strin return configs, rows.Err() } +// ListLuckyGiftRuleConfigVersions 返回同一奖池的完整不可变版本链。主键前缀 +// (app_code,pool_id,rule_version) 可直接反向范围扫描,不会扫描其他应用或奖池的规则行。 +func (r *Repository) ListLuckyGiftRuleConfigVersions(ctx context.Context, poolID string) ([]domain.RuleConfig, error) { + if r == nil || r.db == nil { + return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + appCode := appcode.FromContext(ctx) + poolID = luckyPoolID(poolID) + if appCode == "" || poolID == "" { + return nil, xerr.New(xerr.InvalidArgument, "lucky gift app and pool are required") + } + rows, err := r.db.QueryContext(ctx, luckyRuleConfigSelectSQL+` + ORDER BY rule_version DESC`, appCode, poolID) + if err != nil { + return nil, err + } + defer rows.Close() + configs := make([]domain.RuleConfig, 0) + for rows.Next() { + config, err := scanLuckyGiftRuleConfig(rows) + if err != nil { + return nil, err + } + stages, err := r.getLuckyGiftRuleStages(ctx, r.db, appCode, poolID, config.RuleVersion) + if err != nil { + return nil, err + } + config.Stages = stages + configs = append(configs, config) + } + return configs, rows.Err() +} + +// RollbackLuckyGiftRuleConfig 复制目标快照并追加新版本;旧版本和当前版本都保持不可变。 +// 首先锁最新规则行,再锁实验行,严格沿用常规发布/实验入口的加锁顺序,避免并发发布与回滚互相覆盖。 +func (r *Repository) RollbackLuckyGiftRuleConfig(ctx context.Context, poolID string, targetRuleVersion int64, operatorAdminID int64, nowMS int64) (domain.RuleConfig, error) { + if r == nil || r.db == nil { + return domain.RuleConfig{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + appCode := appcode.FromContext(ctx) + poolID = luckyPoolID(poolID) + if appCode == "" || poolID == "" || targetRuleVersion <= 0 || operatorAdminID <= 0 { + return domain.RuleConfig{}, xerr.New(xerr.InvalidArgument, "lucky gift rollback identity is incomplete") + } + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return domain.RuleConfig{}, err + } + defer func() { _ = tx.Rollback() }() + + current, exists, err := r.getLuckyGiftRuleConfig(ctx, tx, appCode, poolID, true) + if err != nil { + return domain.RuleConfig{}, err + } + if !exists { + return domain.RuleConfig{}, xerr.New(xerr.NotFound, "lucky gift pool config not found") + } + if targetRuleVersion >= current.RuleVersion { + return domain.RuleConfig{}, xerr.New(xerr.InvalidArgument, "rollback target must be older than the current rule version") + } + if _, active, err := r.getActiveLuckyGiftExperiment(ctx, tx, appCode, poolID, true); err != nil { + return domain.RuleConfig{}, err + } else if active { + return domain.RuleConfig{}, xerr.New(xerr.Conflict, "lucky gift pool has an active experiment; end the experiment before rollback") + } + target, exists, err := r.getLuckyGiftRuleConfigVersion(ctx, tx, appCode, poolID, targetRuleVersion, false) + if err != nil { + return domain.RuleConfig{}, err + } + if !exists { + return domain.RuleConfig{}, xerr.New(xerr.NotFound, "lucky gift rollback target version not found") + } + target.CreatedByAdminID = operatorAdminID + // 回滚从当前操作时刻生效;不能沿用历史 effective_from_ms,否则审计会把新版本误显示成旧发布时间。 + target.EffectiveFromMS = 0 + if target.StrategyVersion == domain.StrategyDynamicV3 { + // V3 资金只保留在独立水位账本中。历史快照即使残留 seed,回滚也不能重新注资。 + target.InitialPoolCoins = 0 + } + published, err := r.publishLuckyGiftRuleConfigTx(ctx, tx, target, nowMS) + if err != nil { + return domain.RuleConfig{}, err + } + if err := tx.Commit(); err != nil { + return domain.RuleConfig{}, err + } + return published, nil +} + const luckyRuleConfigSelectSQL = ` SELECT app_code, pool_id, rule_version, enabled, strategy_version, target_rtp_ppm, pool_rate_ppm, profit_rate_ppm, anchor_rate_ppm, diff --git a/services/lucky-gift-service/internal/transport/grpc/lucky_gift_server.go b/services/lucky-gift-service/internal/transport/grpc/lucky_gift_server.go index 3e2171a6..ae7b5339 100644 --- a/services/lucky-gift-service/internal/transport/grpc/lucky_gift_server.go +++ b/services/lucky-gift-service/internal/transport/grpc/lucky_gift_server.go @@ -184,6 +184,31 @@ func (s *AdminLuckyGiftServer) ListLuckyGiftConfigs(ctx context.Context, req *lu return resp, nil } +func (s *AdminLuckyGiftServer) ListLuckyGiftConfigVersions(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigVersionsRequest) (*luckygiftv1.ListLuckyGiftConfigVersionsResponse, error) { + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + configs, err := s.svc.ListConfigVersions(ctx, req.GetPoolId()) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + resp := &luckygiftv1.ListLuckyGiftConfigVersionsResponse{Configs: make([]*luckygiftv1.LuckyGiftRuleConfig, 0, len(configs))} + for _, config := range configs { + resp.Configs = append(resp.Configs, luckyRuleConfigToProto(config)) + } + return resp, nil +} + +func (s *AdminLuckyGiftServer) RollbackLuckyGiftConfig(ctx context.Context, req *luckygiftv1.RollbackLuckyGiftConfigRequest) (*luckygiftv1.RollbackLuckyGiftConfigResponse, error) { + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + config, err := s.svc.RollbackConfig(ctx, req.GetPoolId(), req.GetTargetRuleVersion(), req.GetOperatorAdminId()) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + return &luckygiftv1.RollbackLuckyGiftConfigResponse{ + SourceRuleVersion: req.GetTargetRuleVersion(), + Config: luckyRuleConfigToProto(config), + }, nil +} + func (s *AdminLuckyGiftServer) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) { ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) draws, total, err := s.svc.ListDraws(ctx, domain.DrawQuery{ diff --git a/services/room-service/configs/config.docker.yaml b/services/room-service/configs/config.docker.yaml index c6e545ec..590c9c77 100644 --- a/services/room-service/configs/config.docker.yaml +++ b/services/room-service/configs/config.docker.yaml @@ -81,6 +81,12 @@ rocketmq: topic: "hyapp_user_outbox" consumer_group: "hyapp-room-user-region-sync" consumer_max_reconsume_times: 16 + # 独立消费 wallet owner 事实,Room Cell 装扮失效和资源素材刷新都走持久命令链路。 + wallet_outbox: + enabled: true + topic: "hyapp_wallet_outbox" + consumer_group: "hyapp-room-wallet-decoration-sync" + consumer_max_reconsume_times: 16 # 房间中奖 IM 由 lucky-gift owner 事实可靠派生;独立 group 保证不和其他下游共享消费位点。 lucky_gift_outbox: enabled: true diff --git a/services/room-service/configs/config.tencent.example.yaml b/services/room-service/configs/config.tencent.example.yaml index d685ddd7..68793ce2 100644 --- a/services/room-service/configs/config.tencent.example.yaml +++ b/services/room-service/configs/config.tencent.example.yaml @@ -91,6 +91,12 @@ rocketmq: topic: "hyapp_user_outbox" consumer_group: "hyapp-room-user-region-sync" consumer_max_reconsume_times: 16 + # 线上必须使用 room-service 专属 group;不要复用 notice/statistics 的 wallet 消费位点。 + wallet_outbox: + enabled: true + topic: "hyapp_wallet_outbox" + consumer_group: "hyapp-room-wallet-decoration-sync" + consumer_max_reconsume_times: 16 # 线上必须使用 room-service 自己的消费组;owner outbox 只通过 MQ 进入房间展示链路。 lucky_gift_outbox: enabled: true diff --git a/services/room-service/configs/config.yaml b/services/room-service/configs/config.yaml index 7cef1f31..422c6e1b 100644 --- a/services/room-service/configs/config.yaml +++ b/services/room-service/configs/config.yaml @@ -84,6 +84,12 @@ rocketmq: topic: "hyapp_user_outbox" consumer_group: "hyapp-room-user-region-sync" consumer_max_reconsume_times: 16 + # VIP/资源变更用 room-service 独立消费位点重算当前房间装扮,解绑或禁用后无需等客户端重进房。 + wallet_outbox: + enabled: true + topic: "hyapp_wallet_outbox" + consumer_group: "hyapp-room-wallet-decoration-sync" + consumer_max_reconsume_times: 16 # 幸运礼物 owner 事实由独立消费组派生为房间展示 outbox;不能复用 activity 消费位点或直连幸运礼物库。 lucky_gift_outbox: enabled: true diff --git a/services/room-service/deploy/mysql/initdb/001_room_service.sql b/services/room-service/deploy/mysql/initdb/001_room_service.sql index 68a02559..ce5983ef 100644 --- a/services/room-service/deploy/mysql/initdb/001_room_service.sql +++ b/services/room-service/deploy/mysql/initdb/001_room_service.sql @@ -54,6 +54,8 @@ CREATE TABLE IF NOT EXISTS room_list_entries ( seat_count INT NOT NULL DEFAULT 0 COMMENT '麦位数量', occupied_seat_count INT NOT NULL DEFAULT 0 COMMENT '占用麦位数量', sort_score BIGINT NOT NULL DEFAULT 0 COMMENT '排序分值', + room_border_json JSON NULL COMMENT '当前未过期房间边框资源快照', + room_name_style_json JSON NULL COMMENT '当前未过期彩色房名资源快照', created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', PRIMARY KEY (app_code, room_id), @@ -114,15 +116,82 @@ CREATE TABLE IF NOT EXISTS room_background_images ( app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', background_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '背景图 ID', room_id VARCHAR(64) NOT NULL COMMENT '房间 ID', - image_url VARCHAR(256) NOT NULL COMMENT '背景图 URL', + image_url VARCHAR(1024) NOT NULL COMMENT '背景图 URL', + command_id VARCHAR(128) NULL COMMENT '新版素材保存幂等键;历史行保持 NULL', + object_key VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'COS 对象 key', + content_type VARCHAR(64) NOT NULL DEFAULT '' COMMENT '服务端嗅探 MIME', + media_format VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'jpeg/png/gif/webp', + size_bytes BIGINT NOT NULL DEFAULT 0 COMMENT '文件字节数', + width INT NOT NULL DEFAULT 0 COMMENT '像素宽度', + height INT NOT NULL DEFAULT 0 COMMENT '像素高度', + animated BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否动图', + frame_count INT NOT NULL DEFAULT 0 COMMENT '帧数', + duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '动图总时长', + poster_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选首帧 URL', + thumbnail_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选缩略图 URL', + sha256 CHAR(64) NOT NULL DEFAULT '' COMMENT '原文件 SHA-256', + media_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'active 表示已通过专用上传入口', created_by_user_id BIGINT NOT NULL COMMENT '保存背景图的用户 ID', created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', PRIMARY KEY (background_id), UNIQUE KEY uk_room_background_url (app_code, room_id, image_url), + UNIQUE KEY uk_room_background_command (app_code, room_id, command_id), KEY idx_room_background_list (app_code, room_id, created_at_ms DESC, background_id DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间背景图素材表'; +CREATE TABLE IF NOT EXISTS room_media_upload_registrations ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + room_id VARCHAR(64) NOT NULL COMMENT '目标房间 ID', + command_id VARCHAR(128) NOT NULL COMMENT '专用上传强幂等键', + actor_user_id BIGINT NOT NULL COMMENT '首次授权用户 ID', + purpose VARCHAR(32) NOT NULL COMMENT 'room_background/room_image', + media_payload_sha256 CHAR(64) NOT NULL COMMENT '首次服务端解析媒体事实哈希', + expected_object_key VARCHAR(512) NOT NULL COMMENT 'room owner 确定的 COS key', + object_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT 'PutObject 成功后的访问 URL', + status VARCHAR(32) NOT NULL DEFAULT 'authorized' COMMENT 'authorized/active', + content_type VARCHAR(64) NOT NULL COMMENT '服务端嗅探 MIME', + media_format VARCHAR(16) NOT NULL COMMENT 'jpeg/png/gif/webp', + size_bytes BIGINT NOT NULL COMMENT '原文件字节数', + width INT NOT NULL COMMENT '像素宽度', + height INT NOT NULL COMMENT '像素高度', + animated BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否动图', + frame_count INT NOT NULL COMMENT '帧数', + duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '动图总时长', + sha256 CHAR(64) NOT NULL COMMENT '原文件 SHA-256', + authorized_room_version BIGINT NOT NULL COMMENT '首次授权看到的房间版本', + created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', + PRIMARY KEY (app_code, room_id, command_id), + UNIQUE KEY uk_room_media_upload_object (app_code, expected_object_key), + KEY idx_room_media_upload_status_created (app_code, status, created_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间专用媒体上传授权与完成事实'; + +CREATE TABLE IF NOT EXISTS room_decoration_assignments ( + app_code VARCHAR(32) NOT NULL, + room_id VARCHAR(64) NOT NULL, + decoration_type VARCHAR(32) NOT NULL, + owner_user_id BIGINT NOT NULL, + benefit_code VARCHAR(64) NOT NULL, + resource_id BIGINT NOT NULL, + entitlement_id VARCHAR(128) NOT NULL DEFAULT '', + expires_at_ms BIGINT NOT NULL DEFAULT 0, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, room_id, decoration_type), + KEY idx_room_decoration_resource (app_code, resource_id, room_id, decoration_type), + KEY idx_room_decoration_owner (app_code, owner_user_id, room_id, decoration_type), + KEY idx_room_decoration_expiry (app_code, expires_at_ms, room_id, decoration_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间当前 VIP 装扮反向索引'; + +CREATE TABLE IF NOT EXISTS room_wallet_event_consumption ( + app_code VARCHAR(32) NOT NULL, + event_id VARCHAR(128) NOT NULL, + event_type VARCHAR(64) NOT NULL, + consumed_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, event_id), + KEY idx_room_wallet_event_consumed (app_code, consumed_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='room-service 消费 wallet 装扮事实的本地幂等表'; + CREATE TABLE IF NOT EXISTS room_snapshots ( app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', room_id VARCHAR(64) NOT NULL COMMENT '房间 ID', diff --git a/services/room-service/deploy/mysql/migrations/003_room_vip_media_and_decorations.sql b/services/room-service/deploy/mysql/migrations/003_room_vip_media_and_decorations.sql new file mode 100644 index 00000000..fe38217d --- /dev/null +++ b/services/room-service/deploy/mysql/migrations/003_room_vip_media_and_decorations.sql @@ -0,0 +1,179 @@ +-- 房间新版 VIP 媒体、上传授权与装扮失效读模型。 +-- 性能边界: +-- 1. nullable/default 列使用 MySQL 8 INSTANT,仅短暂持有 metadata lock,不重写历史行;仍建议业务低峰执行。 +-- 2. image_url 扩容与唯一索引需要在线扫描低频 room_background_images,使用 INPLACE/LOCK=NONE。 +-- 3. 唯一索引创建前会主动检查非 NULL command_id 重复组;发现脏数据直接 SIGNAL,拒绝创建冲突索引。 +-- 4. 所有分支先查 information_schema,因此脚本可在发布重试时重复执行。 + +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_room; + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS hyapp_migrate_003_room_vip_media_decorations$$ +CREATE PROCEDURE hyapp_migrate_003_room_vip_media_decorations() +BEGIN + DECLARE duplicate_groups BIGINT DEFAULT 0; + DECLARE oversized_urls BIGINT DEFAULT 0; + + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'room_background_images' + ) THEN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = 'room_background_images' + AND column_name = 'image_url' + AND (data_type <> 'varchar' OR character_maximum_length <> 1024) + ) THEN + SELECT COUNT(*) INTO oversized_urls + FROM room_background_images + WHERE CHAR_LENGTH(image_url) > 1024; + IF oversized_urls > 0 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'room_background_images.image_url contains values longer than 1024'; + END IF; + ALTER TABLE room_background_images + MODIFY COLUMN image_url VARCHAR(1024) NOT NULL, + ALGORITHM=INPLACE, + LOCK=NONE; + END IF; + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'command_id') THEN + ALTER TABLE room_background_images ADD COLUMN command_id VARCHAR(128) NULL COMMENT '新版素材保存幂等键;历史行保持 NULL', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'object_key') THEN + ALTER TABLE room_background_images ADD COLUMN object_key VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'COS 对象 key', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'content_type') THEN + ALTER TABLE room_background_images ADD COLUMN content_type VARCHAR(64) NOT NULL DEFAULT '' COMMENT '服务端嗅探 MIME', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'media_format') THEN + ALTER TABLE room_background_images ADD COLUMN media_format VARCHAR(16) NOT NULL DEFAULT '' COMMENT 'jpeg/png/gif/webp', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'size_bytes') THEN + ALTER TABLE room_background_images ADD COLUMN size_bytes BIGINT NOT NULL DEFAULT 0 COMMENT '文件字节数', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'width') THEN + ALTER TABLE room_background_images ADD COLUMN width INT NOT NULL DEFAULT 0 COMMENT '像素宽度', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'height') THEN + ALTER TABLE room_background_images ADD COLUMN height INT NOT NULL DEFAULT 0 COMMENT '像素高度', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'animated') THEN + ALTER TABLE room_background_images ADD COLUMN animated BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否动图', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'frame_count') THEN + ALTER TABLE room_background_images ADD COLUMN frame_count INT NOT NULL DEFAULT 0 COMMENT '帧数', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'duration_ms') THEN + ALTER TABLE room_background_images ADD COLUMN duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '动图总时长', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'poster_url') THEN + ALTER TABLE room_background_images ADD COLUMN poster_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选首帧 URL', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'thumbnail_url') THEN + ALTER TABLE room_background_images ADD COLUMN thumbnail_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '可选缩略图 URL', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'sha256') THEN + ALTER TABLE room_background_images ADD COLUMN sha256 CHAR(64) NOT NULL DEFAULT '' COMMENT '原文件 SHA-256', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_background_images' AND column_name = 'media_status') THEN + ALTER TABLE room_background_images ADD COLUMN media_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'active 表示已通过专用上传入口', ALGORITHM=INSTANT; + END IF; + + IF NOT EXISTS ( + SELECT 1 FROM information_schema.statistics + WHERE table_schema = DATABASE() AND table_name = 'room_background_images' + AND index_name = 'uk_room_background_command' + ) THEN + SELECT COUNT(*) INTO duplicate_groups + FROM ( + SELECT 1 + FROM room_background_images + WHERE command_id IS NOT NULL + GROUP BY app_code, room_id, command_id + HAVING COUNT(*) > 1 + LIMIT 1 + ) AS duplicate_command_groups; + IF duplicate_groups > 0 THEN + SIGNAL SQLSTATE '45000' + SET MESSAGE_TEXT = 'duplicate room background command_id blocks unique index'; + END IF; + ALTER TABLE room_background_images + ADD UNIQUE KEY uk_room_background_command (app_code, room_id, command_id), + ALGORITHM=INPLACE, + LOCK=NONE; + END IF; + END IF; + + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = 'room_list_entries' + ) THEN + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_list_entries' AND column_name = 'room_border_json') THEN + ALTER TABLE room_list_entries ADD COLUMN room_border_json JSON NULL COMMENT '当前未过期房间边框资源快照', ALGORITHM=INSTANT; + END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = 'room_list_entries' AND column_name = 'room_name_style_json') THEN + ALTER TABLE room_list_entries ADD COLUMN room_name_style_json JSON NULL COMMENT '当前未过期彩色房名资源快照', ALGORITHM=INSTANT; + END IF; + END IF; +END$$ + +CALL hyapp_migrate_003_room_vip_media_decorations()$$ +DROP PROCEDURE IF EXISTS hyapp_migrate_003_room_vip_media_decorations$$ + +DELIMITER ; + +CREATE TABLE IF NOT EXISTS room_media_upload_registrations ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + room_id VARCHAR(64) NOT NULL COMMENT '目标房间 ID', + command_id VARCHAR(128) NOT NULL COMMENT '专用上传强幂等键', + actor_user_id BIGINT NOT NULL COMMENT '首次授权用户 ID', + purpose VARCHAR(32) NOT NULL COMMENT 'room_background/room_image', + media_payload_sha256 CHAR(64) NOT NULL COMMENT '首次服务端解析媒体事实哈希', + expected_object_key VARCHAR(512) NOT NULL COMMENT 'room owner 确定的 COS key', + object_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT 'PutObject 成功后的访问 URL', + status VARCHAR(32) NOT NULL DEFAULT 'authorized' COMMENT 'authorized/active', + content_type VARCHAR(64) NOT NULL COMMENT '服务端嗅探 MIME', + media_format VARCHAR(16) NOT NULL COMMENT 'jpeg/png/gif/webp', + size_bytes BIGINT NOT NULL COMMENT '原文件字节数', + width INT NOT NULL COMMENT '像素宽度', + height INT NOT NULL COMMENT '像素高度', + animated BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否动图', + frame_count INT NOT NULL COMMENT '帧数', + duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '动图总时长', + sha256 CHAR(64) NOT NULL COMMENT '原文件 SHA-256', + authorized_room_version BIGINT NOT NULL COMMENT '首次授权看到的房间版本', + created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', + PRIMARY KEY (app_code, room_id, command_id), + UNIQUE KEY uk_room_media_upload_object (app_code, expected_object_key), + KEY idx_room_media_upload_status_created (app_code, status, created_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间专用媒体上传授权与完成事实'; + +CREATE TABLE IF NOT EXISTS room_decoration_assignments ( + app_code VARCHAR(32) NOT NULL, + room_id VARCHAR(64) NOT NULL, + decoration_type VARCHAR(32) NOT NULL, + owner_user_id BIGINT NOT NULL, + benefit_code VARCHAR(64) NOT NULL, + resource_id BIGINT NOT NULL, + entitlement_id VARCHAR(128) NOT NULL DEFAULT '', + expires_at_ms BIGINT NOT NULL DEFAULT 0, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, room_id, decoration_type), + KEY idx_room_decoration_resource (app_code, resource_id, room_id, decoration_type), + KEY idx_room_decoration_owner (app_code, owner_user_id, room_id, decoration_type), + KEY idx_room_decoration_expiry (app_code, expires_at_ms, room_id, decoration_type) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间当前 VIP 装扮反向索引'; + +CREATE TABLE IF NOT EXISTS room_wallet_event_consumption ( + app_code VARCHAR(32) NOT NULL, + event_id VARCHAR(128) NOT NULL, + event_type VARCHAR(64) NOT NULL, + consumed_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, event_id), + KEY idx_room_wallet_event_consumed (app_code, consumed_at_ms) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='room-service 消费 wallet 装扮事实的本地幂等表'; diff --git a/services/room-service/internal/app/app.go b/services/room-service/internal/app/app.go index 32708d6f..c98a94e9 100644 --- a/services/room-service/internal/app/app.go +++ b/services/room-service/internal/app/app.go @@ -25,6 +25,7 @@ import ( "hyapp/pkg/tencentim" "hyapp/pkg/tencentrtc" "hyapp/pkg/usermq" + "hyapp/pkg/walletmq" "hyapp/services/room-service/internal/config" "hyapp/services/room-service/internal/healthcheck" "hyapp/services/room-service/internal/integration" @@ -66,7 +67,7 @@ type App struct { healthHTTP *healthhttp.Server // mqProducers 在 outbox worker 启动前打开,承载 fanout 和延迟发射唤醒。 mqProducers []*rocketmqx.Producer - // mqConsumers 在 gRPC 服务启动前订阅,承接火箭唤醒、用户区域同步和幸运礼物 owner 事实。 + // mqConsumers 在 gRPC 服务启动前订阅,承接火箭唤醒、用户区域、wallet 装扮失效和幸运礼物 owner 事实。 mqConsumers []*rocketmqx.Consumer // workers 统一控制本节点后台 worker,关闭时必须先停止 worker 再释放 MySQL/Redis。 workers *serviceapp.BackgroundGroup @@ -178,6 +179,13 @@ func New(cfg config.Config) (*App, error) { concreteTencentPublisher := integration.NewTencentIMPublisher(tencentClient) tencentPublisher = concreteTencentPublisher roomPublisher = concreteTencentPublisher + // 图片消息和 VIP 房间装扮都是用户可见的持久事实,必须由 durable room_outbox 补偿腾讯 IM; + // 只过滤这两类事件,避免把现有 direct-IM 展示事件整体接入后制造双消息。 + outboxPublishers = append(outboxPublishers, integration.NewEventTypeOutboxPublisher( + concreteTencentPublisher, + "RoomImageMessageSent", + "RoomDecorationChanged", + )) } if cfg.TencentRTC.Enabled { rtcClient, err := tencentrtc.NewRESTClient(cfg.TencentRTC.RESTConfig()) @@ -192,7 +200,7 @@ func New(cfg config.Config) (*App, error) { } mqProducers := make([]*rocketmqx.Producer, 0, 2) - mqConsumers := make([]*rocketmqx.Consumer, 0, 4) + mqConsumers := make([]*rocketmqx.Consumer, 0, 5) var rocketLaunchScheduler integration.RoomRocketLaunchScheduler if cfg.OutboxWorker.PublishMode == config.OutboxPublishModeDirect || cfg.OutboxWorker.PublishMode == config.OutboxPublishModeDual { if activityConn != nil { @@ -297,6 +305,46 @@ func New(cfg config.Config) (*App, error) { } mqConsumers = append(mqConsumers, userConsumer) } + if cfg.RocketMQ.WalletOutbox.Enabled { + // 兼容 legacy Tag 与 typed Tag 的滚动窗口;独立 group 只消费会改变当前房间装扮的 owner 事实。 + walletTagExpression, err := walletmq.LegacyCompatibleTagExpression( + "ResourceChanged", + "ResourceGrantRevoked", + "UserResourceChanged", + "VipEffectiveChanged", + "VipProgramChanged", + ) + if err != nil { + _ = repository.Close() + closeClientConn(activityConn) + _ = walletConn.Close() + _ = redisClient.Close() + return nil, err + } + walletConsumer, err := rocketmqx.NewConsumer(rocketMQConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.WalletOutbox.ConsumerGroup, cfg.RocketMQ.WalletOutbox.ConsumerMaxReconsumeTimes, 0)) + if err != nil { + _ = repository.Close() + closeClientConn(activityConn) + _ = walletConn.Close() + _ = redisClient.Close() + return nil, err + } + if err := walletConsumer.Subscribe(cfg.RocketMQ.WalletOutbox.Topic, walletTagExpression, func(ctx context.Context, message rocketmqx.ConsumedMessage) error { + walletMessage, err := walletmq.DecodeWalletOutboxMessage(message.Body) + if err != nil { + return err + } + return svc.HandleWalletDecorationEvent(ctx, walletMessage) + }); err != nil { + _ = walletConsumer.Shutdown() + _ = repository.Close() + closeClientConn(activityConn) + _ = walletConn.Close() + _ = redisClient.Close() + return nil, err + } + mqConsumers = append(mqConsumers, walletConsumer) + } if cfg.RocketMQ.LuckyGiftOutbox.Enabled { luckyGiftConsumer, err := rocketmqx.NewConsumer(luckyGiftRocketMQConsumerConfig(cfg.RocketMQ)) if err != nil { diff --git a/services/room-service/internal/config/config.go b/services/room-service/internal/config/config.go index 1a7215cc..83fba42f 100644 --- a/services/room-service/internal/config/config.go +++ b/services/room-service/internal/config/config.go @@ -156,6 +156,7 @@ type RocketMQConfig struct { RoomOutbox RoomOutboxMQConfig `yaml:"room_outbox"` RocketLaunch RocketLaunchMQConfig `yaml:"rocket_launch"` UserOutbox UserOutboxMQConfig `yaml:"user_outbox"` + WalletOutbox WalletOutboxMQConfig `yaml:"wallet_outbox"` LuckyGiftOutbox LuckyGiftOutboxMQConfig `yaml:"lucky_gift_outbox"` } @@ -191,6 +192,15 @@ type UserOutboxMQConfig struct { ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"` } +// WalletOutboxMQConfig 控制 wallet owner 的 VIP/资源变化事实消费。 +// room-service 使用独立 group 更新 Room Cell 装扮,不能复用 notice/statistics 的消费位点。 +type WalletOutboxMQConfig struct { + Enabled bool `yaml:"enabled"` + Topic string `yaml:"topic"` + ConsumerGroup string `yaml:"consumer_group"` + ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"` +} + // LuckyGiftOutboxMQConfig 控制 room-service 消费 lucky-gift-service 已提交的中奖事实。 // 该 consumer 只派生房间展示 outbox,不读取幸运礼物库,也不修改 Room Cell 状态。 type LuckyGiftOutboxMQConfig struct { @@ -381,6 +391,12 @@ func defaultRocketMQConfig() RocketMQConfig { ConsumerGroup: "hyapp-room-user-region-sync", ConsumerMaxReconsumeTimes: 16, }, + WalletOutbox: WalletOutboxMQConfig{ + Enabled: false, + Topic: "hyapp_wallet_outbox", + ConsumerGroup: "hyapp-room-wallet-decoration-sync", + ConsumerMaxReconsumeTimes: 16, + }, LuckyGiftOutbox: LuckyGiftOutboxMQConfig{ Enabled: false, Topic: "hyapp_lucky_gift_outbox", @@ -629,6 +645,15 @@ func normalizeRocketMQConfig(cfg RocketMQConfig, publishMode string) (RocketMQCo if cfg.UserOutbox.ConsumerMaxReconsumeTimes <= 0 { cfg.UserOutbox.ConsumerMaxReconsumeTimes = defaults.UserOutbox.ConsumerMaxReconsumeTimes } + if cfg.WalletOutbox.Topic = strings.TrimSpace(cfg.WalletOutbox.Topic); cfg.WalletOutbox.Topic == "" { + cfg.WalletOutbox.Topic = defaults.WalletOutbox.Topic + } + if cfg.WalletOutbox.ConsumerGroup = strings.TrimSpace(cfg.WalletOutbox.ConsumerGroup); cfg.WalletOutbox.ConsumerGroup == "" { + cfg.WalletOutbox.ConsumerGroup = defaults.WalletOutbox.ConsumerGroup + } + if cfg.WalletOutbox.ConsumerMaxReconsumeTimes <= 0 { + cfg.WalletOutbox.ConsumerMaxReconsumeTimes = defaults.WalletOutbox.ConsumerMaxReconsumeTimes + } if cfg.LuckyGiftOutbox.Topic = strings.TrimSpace(cfg.LuckyGiftOutbox.Topic); cfg.LuckyGiftOutbox.Topic == "" { cfg.LuckyGiftOutbox.Topic = defaults.LuckyGiftOutbox.Topic } @@ -638,7 +663,7 @@ func normalizeRocketMQConfig(cfg RocketMQConfig, publishMode string) (RocketMQCo if cfg.LuckyGiftOutbox.ConsumerMaxReconsumeTimes <= 0 { cfg.LuckyGiftOutbox.ConsumerMaxReconsumeTimes = defaults.LuckyGiftOutbox.ConsumerMaxReconsumeTimes } - if cfg.RoomOutbox.Enabled || cfg.RocketLaunch.Enabled || cfg.UserOutbox.Enabled || cfg.LuckyGiftOutbox.Enabled { + if cfg.RoomOutbox.Enabled || cfg.RocketLaunch.Enabled || cfg.UserOutbox.Enabled || cfg.WalletOutbox.Enabled || cfg.LuckyGiftOutbox.Enabled { cfg.Enabled = true } if publishMode == OutboxPublishModeMQ || publishMode == OutboxPublishModeDual { diff --git a/services/room-service/internal/integration/clients.go b/services/room-service/internal/integration/clients.go index d3f4920c..3b2625c8 100644 --- a/services/room-service/internal/integration/clients.go +++ b/services/room-service/internal/integration/clients.go @@ -25,6 +25,18 @@ type WalletClient interface { GetMyVip(ctx context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error) } +// WalletVIPBenefitClient 是新版 VIP 单项权益校验的可选能力。它独立于 WalletClient, +// 避免房间测试里的轻量账务 fake 因新增只读 RPC 被迫实现无关方法;生产 gRPC client 必须实现。 +type WalletVIPBenefitClient interface { + CheckVipBenefit(ctx context.Context, req *walletv1.CheckVipBenefitRequest) (*walletv1.CheckVipBenefitResponse, error) +} + +// WalletUserResourceClient 是房间装扮读取用户有效资源的可选能力。资源归 wallet owner, +// room-service 只钉住应用时的展示快照,不能复制 entitlement 有效期和数量算法。 +type WalletUserResourceClient interface { + ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) +} + // LuckyGiftClient 抽象 room-service 对 lucky-gift-service 幸运礼物抽奖边界的同步依赖。 type LuckyGiftClient interface { // CheckLuckyGift 在扣费前确认奖池规则可用;失败时不能扣费。 diff --git a/services/room-service/internal/integration/clients_grpc.go b/services/room-service/internal/integration/clients_grpc.go index 014f6a1f..289cf81e 100644 --- a/services/room-service/internal/integration/clients_grpc.go +++ b/services/room-service/internal/integration/clients_grpc.go @@ -50,6 +50,16 @@ func (c *grpcWalletClient) GetMyVip(ctx context.Context, req *walletv1.GetMyVipR return c.client.GetMyVip(ctx, req) } +// CheckVipBenefit 使用 wallet 已求值的最终权益;room-service 不按 VIP 等级反推。 +func (c *grpcWalletClient) CheckVipBenefit(ctx context.Context, req *walletv1.CheckVipBenefitRequest) (*walletv1.CheckVipBenefitResponse, error) { + return c.client.CheckVipBenefit(ctx, req) +} + +// ListUserResources 返回 wallet 判定仍有效的 entitlement 和资源目录快照。 +func (c *grpcWalletClient) ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) { + return c.client.ListUserResources(ctx, req) +} + type grpcLuckyGiftClient struct { client luckygiftv1.LuckyGiftServiceClient } diff --git a/services/room-service/internal/integration/outbox_filter.go b/services/room-service/internal/integration/outbox_filter.go new file mode 100644 index 00000000..f4ded7f7 --- /dev/null +++ b/services/room-service/internal/integration/outbox_filter.go @@ -0,0 +1,35 @@ +package integration + +import ( + "context" + "strings" + + roomeventsv1 "hyapp.local/api/proto/events/room/v1" +) + +// eventTypeOutboxPublisher 只把明确需要可靠腾讯 IM 补偿的展示事件转交给下游。 +// 现有房间事件大多已有 direct-IM lane,不能无差别接入否则会给旧客户端制造双消息。 +type eventTypeOutboxPublisher struct { + inner OutboxPublisher + allowedTypes map[string]struct{} +} + +func NewEventTypeOutboxPublisher(inner OutboxPublisher, eventTypes ...string) OutboxPublisher { + allowed := make(map[string]struct{}, len(eventTypes)) + for _, eventType := range eventTypes { + if eventType = strings.TrimSpace(eventType); eventType != "" { + allowed[eventType] = struct{}{} + } + } + return &eventTypeOutboxPublisher{inner: inner, allowedTypes: allowed} +} + +func (p *eventTypeOutboxPublisher) PublishOutboxEvent(ctx context.Context, envelope *roomeventsv1.EventEnvelope) error { + if p == nil || p.inner == nil || envelope == nil { + return nil + } + if _, allowed := p.allowedTypes[envelope.GetEventType()]; !allowed { + return nil + } + return p.inner.PublishOutboxEvent(ctx, envelope) +} diff --git a/services/room-service/internal/integration/tencent_im.go b/services/room-service/internal/integration/tencent_im.go index af3530b9..7789e1df 100644 --- a/services/room-service/internal/integration/tencent_im.go +++ b/services/room-service/internal/integration/tencent_im.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "google.golang.org/protobuf/proto" roomeventsv1 "hyapp.local/api/proto/events/room/v1" @@ -243,6 +244,46 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room "background_id": fmt.Sprintf("%d", body.GetBackgroundId()), "room_background_url": body.GetRoomBackgroundUrl(), } + appendRoomMediaAttributes(base.Attributes, body.GetMedia(), "") + return base, true, nil + case "RoomDecorationChanged": + var body roomeventsv1.RoomDecorationChanged + if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil { + return tencentim.RoomEvent{}, false, err + } + if body.GetExpiresAtMs() > 0 && time.Now().UTC().UnixMilli() >= body.GetExpiresAtMs() { + // 装扮应用事实继续留在 outbox/MQ,但补偿 IM 在资格到期后不再让客户端恢复失效视觉。 + return tencentim.RoomEvent{}, false, nil + } + base.ActorUserID = body.GetActorUserId() + colors, err := json.Marshal(body.GetTextColors()) + if err != nil { + return tencentim.RoomEvent{}, false, err + } + stops, err := json.Marshal(body.GetStops()) + if err != nil { + return tencentim.RoomEvent{}, false, err + } + base.Attributes = map[string]string{ + "decoration_type": body.GetDecorationType(), "resource_id": fmt.Sprintf("%d", body.GetResourceId()), + "resource_code": body.GetResourceCode(), "resource_type": body.GetResourceType(), "name": body.GetName(), + "asset_url": body.GetAssetUrl(), "preview_url": body.GetPreviewUrl(), "animation_url": body.GetAnimationUrl(), + "format": body.GetFormat(), "metadata_json": body.GetMetadataJson(), "text_colors": string(colors), + "stops": string(stops), "angle_degrees": fmt.Sprintf("%g", body.GetAngleDegrees()), + "entitlement_id": body.GetEntitlementId(), "expires_at_ms": fmt.Sprintf("%d", body.GetExpiresAtMs()), + } + return base, true, nil + case "RoomImageMessageSent": + var body roomeventsv1.RoomImageMessageSent + if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil { + return tencentim.RoomEvent{}, false, err + } + base.ActorUserID = body.GetSenderUserId() + base.Attributes = map[string]string{ + "message_id": body.GetMessageId(), "command_id": body.GetCommandId(), + "sender_user_id": fmt.Sprintf("%d", body.GetSenderUserId()), + } + appendRoomMediaAttributes(base.Attributes, body.GetImage(), "image_") return base, true, nil case "RoomChatEnabledChanged": // 公屏开关事件提示客户端更新输入态,真正发言仍由 CheckSpeakPermission 拦截。 @@ -626,6 +667,10 @@ func eventTypeForClient(eventType string) string { return "room_profile_updated" case "RoomBackgroundChanged": return "room_background_changed" + case "RoomDecorationChanged": + return "room_decoration_changed" + case "RoomImageMessageSent": + return "room_image_message" case "RoomChatEnabledChanged": return "room_chat_enabled_changed" case "RoomPasswordChanged": @@ -662,3 +707,22 @@ func eventTypeForClient(eventType string) string { return eventType } } + +func appendRoomMediaAttributes(attributes map[string]string, media *roomeventsv1.RoomMediaSnapshot, prefix string) { + if attributes == nil || media == nil { + return + } + attributes[prefix+"url"] = media.GetUrl() + attributes[prefix+"object_key"] = media.GetObjectKey() + attributes[prefix+"content_type"] = media.GetContentType() + attributes[prefix+"format"] = media.GetFormat() + attributes[prefix+"size_bytes"] = fmt.Sprintf("%d", media.GetSizeBytes()) + attributes[prefix+"width"] = fmt.Sprintf("%d", media.GetWidth()) + attributes[prefix+"height"] = fmt.Sprintf("%d", media.GetHeight()) + attributes[prefix+"animated"] = fmt.Sprintf("%t", media.GetAnimated()) + attributes[prefix+"frame_count"] = fmt.Sprintf("%d", media.GetFrameCount()) + attributes[prefix+"duration_ms"] = fmt.Sprintf("%d", media.GetDurationMs()) + attributes[prefix+"poster_url"] = media.GetPosterUrl() + attributes[prefix+"thumbnail_url"] = media.GetThumbnailUrl() + attributes[prefix+"sha256"] = media.GetSha256() +} diff --git a/services/room-service/internal/room/command/command.go b/services/room-service/internal/room/command/command.go index ae8f1681..f430fc7d 100644 --- a/services/room-service/internal/room/command/command.go +++ b/services/room-service/internal/room/command/command.go @@ -4,6 +4,8 @@ package command import ( "encoding/json" "fmt" + + "hyapp/services/room-service/internal/room/state" ) // Command 统一表达可写入 command log 的房间命令。 @@ -109,10 +111,48 @@ type SetRoomBackground struct { BackgroundID int64 `json:"background_id"` // BackgroundURL 是提交前从房间背景素材表解析出的确定性快照,恢复时不再回查列表表。 BackgroundURL string `json:"background_url"` + // Background 保存完整媒体事实;旧 command log 缺字段时恢复继续使用 BackgroundID/BackgroundURL。 + Background state.RoomBackground `json:"background,omitempty"` } func (SetRoomBackground) Type() string { return "set_room_background" } +// ApplyRoomDecoration 定义房主把有效 wallet entitlement 应用到指定房间的请求。 +type ApplyRoomDecoration struct { + Base + // DecorationType 是客户端动作语义,只接受 room_border/colored_room_name。 + DecorationType string `json:"decoration_type"` + // ResourceID/EntitlementID 是幂等载荷;Resource 是 wallet 校验后固化的恢复快照。 + ResourceID int64 `json:"resource_id"` + EntitlementID string `json:"entitlement_id,omitempty"` + Resource state.RoomDecoration `json:"resource,omitempty"` +} + +func (ApplyRoomDecoration) Type() string { return "apply_room_decoration" } + +// ReconcileRoomDecoration 由 wallet owner 事实触发,用当前 benefit/resource/entitlement 状态更新或清除房间装扮。 +type ReconcileRoomDecoration struct { + Base + DecorationType string `json:"decoration_type"` + ExpectedResourceID int64 `json:"expected_resource_id"` + BenefitCode string `json:"benefit_code"` + SourceEventID string `json:"source_event_id"` + Active bool `json:"active"` + Resource state.RoomDecoration `json:"resource,omitempty"` +} + +func (ReconcileRoomDecoration) Type() string { return "reconcile_room_decoration" } + +// SendRoomImage 定义由 room-service 权限 owner 接受并写 outbox 的图片消息。 +// 图片本身已由 gateway 专用上传入口解析,命令仍会复核归属、房态、禁言和 VIP 权益。 +type SendRoomImage struct { + Base + MessageID string `json:"message_id"` + Image state.RoomMedia `json:"image"` +} + +func (SendRoomImage) Type() string { return "send_room_image" } + // JoinRoom 定义业务进房请求。 type EntryVehicleSnapshot struct { ResourceID int64 `json:"resource_id,omitempty"` @@ -739,6 +779,13 @@ func IdempotencyPayload(payload []byte) ([]byte, error) { delete(values, "publish_deadline_ms") // ConfirmMicPublishing 的接受时间由 room-service 生成,不能参与客户端请求幂等语义。 delete(values, "accepted_event_time_ms") + // SetRoomBackground/ApplyRoomDecoration 的完整资源快照来自 owner 表或 wallet,不属于客户端幂等载荷。 + // 客户端只按 background_id 或 decoration_type/resource_id/entitlement_id 表达动作。 + delete(values, "background_url") + delete(values, "background") + delete(values, "resource") + // 图片 message_id 由 room_id + command_id 确定性生成;真正的图片属性仍参与冲突判断。 + delete(values, "message_id") return json.Marshal(values) } @@ -772,6 +819,12 @@ func Deserialize(commandType string, payload []byte) (Command, error) { cmd = &UpdateRoomProfile{} case SetRoomBackground{}.Type(): cmd = &SetRoomBackground{} + case ApplyRoomDecoration{}.Type(): + cmd = &ApplyRoomDecoration{} + case ReconcileRoomDecoration{}.Type(): + cmd = &ReconcileRoomDecoration{} + case SendRoomImage{}.Type(): + cmd = &SendRoomImage{} case JoinRoom{}.Type(): cmd = &JoinRoom{} case RoomHeartbeat{}.Type(): diff --git a/services/room-service/internal/room/service/background.go b/services/room-service/internal/room/service/background.go index 643d4335..13b41d5b 100644 --- a/services/room-service/internal/room/service/background.go +++ b/services/room-service/internal/room/service/background.go @@ -2,12 +2,14 @@ package service import ( "context" + "errors" "strings" "time" "unicode/utf8" roomeventsv1 "hyapp.local/api/proto/events/room/v1" roomv1 "hyapp.local/api/proto/room/v1" + "hyapp/pkg/giftlimits" "hyapp/pkg/roomid" "hyapp/pkg/tencentim" "hyapp/pkg/xerr" @@ -21,28 +23,82 @@ import ( // 该动作只维护低频素材表,不改变 Room Cell 当前背景;真正生效必须调用 SetRoomBackground。 func (s *Service) SaveRoomBackground(ctx context.Context, req *roomv1.SaveRoomBackgroundRequest) (*roomv1.SaveRoomBackgroundResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) - roomID, actorUserID, imageURL, err := normalizeSaveRoomBackground(req) + roomID, actorUserID, commandID, media, err := normalizeSaveRoomBackground(req) if err != nil { return nil, err } + if commandID != "" && !giftlimits.ValidCommandID(commandID) { + return nil, xerr.New(xerr.InvalidArgument, "command_id is invalid") + } + + imageURL := media.URL + replayCommandID := commandID + if replayCommandID == "" { + // legacy_timed 没有客户端 command_id;这里先算出与首次提交一致的稳定键,确保旧成功也能先于可变权限重放。 + replayCommandID = legacyRoomBackgroundCommandID(req.GetMeta().GetAppCode(), roomID, actorUserID, imageURL) + } + if reader, ok := s.repository.(RoomBackgroundCommandReader); ok { + existing, exists, readErr := reader.GetRoomBackgroundByCommand(ctx, roomID, replayCommandID) + if readErr != nil { + return nil, readErr + } + if exists { + if !sameRoomBackgroundSaveRequest(existing, actorUserID, imageURL, media) { + return nil, xerr.New(xerr.IdempotencyConflict, "command_id payload conflict") + } + // 已成功保存是不可变事实。房间随后关闭、owner 元数据变化或 VIP 到期都不能把同一重试改成失败。 + return &roomv1.SaveRoomBackgroundResponse{ + Background: roomBackgroundToProto(existing), + ServerTimeMs: s.clock.Now().UnixMilli(), + }, nil + } + } if err := s.requireRoomOwnerMeta(ctx, roomID, actorUserID); err != nil { return nil, err } - if err := s.requireCustomRoomBackgroundBenefit(ctx, req.GetMeta().GetRequestId(), actorUserID); err != nil { - // 保存素材本身就会形成持久数据,P1 App 必须在写库前校验当前 effective benefit。 + access, err := s.loadEffectiveVIPAccess(ctx, req.GetMeta().GetRequestId(), actorUserID) + if err != nil { return nil, err } + if access.programType == tieredPrivilegeVIPProgram { + if err := s.requireCustomRoomBackgroundBenefit(ctx, req.GetMeta().GetRequestId(), actorUserID, "save_room_background"); err != nil { + // Fami P1 的素材保存会形成持久数据,必须在写库前校验当前精确 benefit。 + return nil, err + } + if !giftlimits.ValidCommandID(commandID) { + return nil, xerr.New(xerr.InvalidArgument, "command_id is invalid") + } + if err := validateRoomMedia(req.GetMeta().GetAppCode(), roomID, actorUserID, roomMediaPurposeBackground, media); err != nil { + return nil, err + } + if err := s.requireActiveRoomMediaUpload(ctx, roomID, actorUserID, roomMediaPurposeBackground, commandID, media); err != nil { + // URL/key 前缀只能证明路径归属;这里还要用 active registration 锁定首次嗅探的哈希、尺寸和格式。 + return nil, err + } + } else { + // Lalu legacy_timed 保留历史 image_url-only 协议;服务端按 URL 生成稳定幂等键,空 Media 表示未经过新版专用上传。 + if media.URL == "" || utf8.RuneCountInString(media.URL) > maxRoomBackgroundRunes { + return nil, xerr.New(xerr.InvalidArgument, "image_url is invalid") + } + commandID = replayCommandID + media = state.RoomMedia{} + } nowMS := s.clock.Now().UnixMilli() background, err := s.repository.SaveRoomBackground(ctx, RoomBackgroundImage{ AppCode: req.GetMeta().GetAppCode(), RoomID: roomID, ImageURL: imageURL, + CommandID: commandID, + Media: media, CreatedByUserID: actorUserID, CreatedAtMS: nowMS, UpdatedAtMS: nowMS, }) if err != nil { + if errors.Is(err, ErrRoomBackgroundCommandConflict) { + return nil, xerr.New(xerr.IdempotencyConflict, "command_id payload conflict") + } return nil, err } @@ -76,6 +132,7 @@ func (s *Service) ListRoomBackgrounds(ctx context.Context, req *roomv1.ListRoomB return &roomv1.ListRoomBackgroundsResponse{ Backgrounds: roomBackgroundsToProto(backgrounds), SelectedBackgroundUrl: snapshot.GetRoomExt()[roomExtBackgroundKey], + SelectedBackground: snapshot.GetActiveBackground(), ServerTimeMs: s.clock.Now().UnixMilli(), }, nil } @@ -88,21 +145,42 @@ func (s *Service) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBack if err != nil { return nil, err } - if err := s.requireRoomOwnerMeta(ctx, roomID, actorUserID); err != nil { - return nil, err + cmd := command.SetRoomBackground{ + Base: baseFromMeta(req.GetMeta()), + BackgroundID: backgroundID, } - background, exists, err := s.repository.GetRoomBackground(ctx, roomID, backgroundID) + _, record, seen, err := s.commandRecordForIdempotency(ctx, cmd) if err != nil { return nil, err } - if !exists { - return nil, xerr.New(xerr.NotFound, "room background not found") - } - cmd := command.SetRoomBackground{ - Base: baseFromMeta(req.GetMeta()), - BackgroundID: background.BackgroundID, - BackgroundURL: background.ImageURL, + var background RoomBackgroundImage + if seen { + stored, deserializeErr := command.Deserialize(record.CommandType, record.Payload) + if deserializeErr != nil { + return nil, deserializeErr + } + storedCommand, ok := stored.(*command.SetRoomBackground) + if !ok { + return nil, xerr.New(xerr.Internal, "stored room background command is invalid") + } + cmd = *storedCommand + background = roomBackgroundFromSetCommand(cmd) + } else { + if err := s.requireRoomOwnerMeta(ctx, roomID, actorUserID); err != nil { + return nil, err + } + var exists bool + background, exists, err = s.repository.GetRoomBackground(ctx, roomID, backgroundID) + if err != nil { + return nil, err + } + if !exists { + return nil, xerr.New(xerr.NotFound, "room background not found") + } + cmd.BackgroundID = background.BackgroundID + cmd.BackgroundURL = background.ImageURL + cmd.Background = roomBackgroundState(background) } result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { if err := requireActiveRoom(current); err != nil { @@ -111,23 +189,31 @@ func (s *Service) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBack if current.OwnerUserID != cmd.ActorUserID() { return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "permission denied") } - if err := s.requireCustomRoomBackgroundBenefit(ctx, req.GetMeta().GetRequestId(), cmd.ActorUserID()); err != nil { + if err := s.requireCustomRoomBackgroundBenefit(ctx, req.GetMeta().GetRequestId(), cmd.ActorUserID(), "set_room_background"); err != nil { // 放在 mutate 回调内可让同一 command_id 的已提交重试先命中幂等,不会因 VIP 后续到期改写旧命令结果。 return mutationResult{}, nil, err } if current.RoomExt == nil { current.RoomExt = make(map[string]string) } - if current.RoomExt[roomExtBackgroundKey] == cmd.BackgroundURL { + if (current.ActiveBackground != nil && current.ActiveBackground.BackgroundID == cmd.BackgroundID) || + (current.ActiveBackground == nil && current.RoomExt[roomExtBackgroundKey] == cmd.BackgroundURL) { return mutationResult{snapshot: current.ToProto()}, nil, nil } current.RoomExt[roomExtBackgroundKey] = cmd.BackgroundURL + if cmd.Background.Media.URL != "" { + backgroundSnapshot := cmd.Background + current.ActiveBackground = &backgroundSnapshot + } else { + current.ActiveBackground = nil + } current.Version++ backgroundEvent, err := outbox.Build(current.RoomID, "RoomBackgroundChanged", current.Version, now, &roomeventsv1.RoomBackgroundChanged{ ActorUserId: cmd.ActorUserID(), BackgroundId: cmd.BackgroundID, RoomBackgroundUrl: current.RoomExt[roomExtBackgroundKey], + Media: roomMediaEventSnapshot(cmd.Background.Media), }) if err != nil { return mutationResult{}, nil, err @@ -158,29 +244,66 @@ func (s *Service) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBack }, nil } -func normalizeSaveRoomBackground(req *roomv1.SaveRoomBackgroundRequest) (string, int64, string, error) { +func sameRoomBackgroundSaveRequest(existing RoomBackgroundImage, actorUserID int64, imageURL string, media state.RoomMedia) bool { + if existing.CreatedByUserID != actorUserID || strings.TrimSpace(existing.ImageURL) != strings.TrimSpace(imageURL) { + return false + } + if strings.TrimSpace(media.ObjectKey) != "" { + // 新版媒体事实全部由 gateway 解析并由 room-service 校验;任一字段变化都代表 command_id 被复用。 + return existing.Media == media + } + legacyCandidate := media + legacyCandidate.URL = "" + // legacy_timed 只接受 image_url,其余媒体字段必须同时为空,不能借旧成功绕过新版专用上传校验。 + return existing.Media == (state.RoomMedia{}) && legacyCandidate == (state.RoomMedia{}) +} + +func roomBackgroundFromSetCommand(cmd command.SetRoomBackground) RoomBackgroundImage { + backgroundID := cmd.Background.BackgroundID + if backgroundID <= 0 { + backgroundID = cmd.BackgroundID + } + roomID := strings.TrimSpace(cmd.Background.RoomID) + if roomID == "" { + roomID = cmd.RoomID() + } + createdByUserID := cmd.Background.CreatedByUserID + if createdByUserID <= 0 { + createdByUserID = cmd.ActorUserID() + } + // command log 固化第一次应用时的素材快照;旧记录没有 Background 时继续用 BackgroundURL 返回历史结果。 + return RoomBackgroundImage{ + AppCode: cmd.AppCode, BackgroundID: backgroundID, RoomID: roomID, + ImageURL: strings.TrimSpace(cmd.BackgroundURL), CommandID: cmd.Background.CommandID, Media: cmd.Background.Media, + CreatedByUserID: createdByUserID, CreatedAtMS: cmd.Background.CreatedAtMS, UpdatedAtMS: cmd.Background.UpdatedAtMS, + } +} + +func normalizeSaveRoomBackground(req *roomv1.SaveRoomBackgroundRequest) (string, int64, string, state.RoomMedia, error) { if req == nil || req.GetMeta() == nil { - return "", 0, "", xerr.New(xerr.InvalidArgument, "request is required") + return "", 0, "", state.RoomMedia{}, xerr.New(xerr.InvalidArgument, "request is required") } roomID := strings.TrimSpace(req.GetRoomId()) if roomID == "" { roomID = strings.TrimSpace(req.GetMeta().GetRoomId()) } if !roomid.ValidStringID(roomID) { - return "", 0, "", xerr.New(xerr.InvalidArgument, "room_id is invalid") + return "", 0, "", state.RoomMedia{}, xerr.New(xerr.InvalidArgument, "room_id is invalid") } actorUserID := req.GetMeta().GetActorUserId() if actorUserID <= 0 { - return "", 0, "", xerr.New(xerr.InvalidArgument, "actor_user_id is required") + return "", 0, "", state.RoomMedia{}, xerr.New(xerr.InvalidArgument, "actor_user_id is required") } - imageURL := strings.TrimSpace(req.GetImageUrl()) - if imageURL == "" { - return "", 0, "", xerr.New(xerr.InvalidArgument, "image_url is required") + commandID := strings.TrimSpace(req.GetMeta().GetCommandId()) + media := roomMediaFromProto(req.GetMedia()) + requestImageURL := strings.TrimSpace(req.GetImageUrl()) + if media.URL != "" && requestImageURL != "" && media.URL != requestImageURL { + return "", 0, "", state.RoomMedia{}, xerr.New(xerr.InvalidArgument, "image_url does not match media.url") } - if utf8.RuneCountInString(imageURL) > maxRoomBackgroundRunes { - return "", 0, "", xerr.New(xerr.InvalidArgument, "image_url is too long") + if media.URL == "" { + media.URL = requestImageURL } - return roomID, actorUserID, imageURL, nil + return roomID, actorUserID, commandID, media, nil } func normalizeListRoomBackgrounds(req *roomv1.ListRoomBackgroundsRequest) (string, int64, error) { @@ -235,25 +358,3 @@ func (s *Service) requireRoomOwnerMeta(ctx context.Context, roomID string, actor } return nil } - -func roomBackgroundToProto(background RoomBackgroundImage) *roomv1.RoomBackgroundImage { - if background.BackgroundID == 0 { - return nil - } - return &roomv1.RoomBackgroundImage{ - BackgroundId: background.BackgroundID, - RoomId: background.RoomID, - ImageUrl: background.ImageURL, - CreatedByUserId: background.CreatedByUserID, - CreatedAtMs: background.CreatedAtMS, - UpdatedAtMs: background.UpdatedAtMS, - } -} - -func roomBackgroundsToProto(backgrounds []RoomBackgroundImage) []*roomv1.RoomBackgroundImage { - out := make([]*roomv1.RoomBackgroundImage, 0, len(backgrounds)) - for _, background := range backgrounds { - out = append(out, roomBackgroundToProto(background)) - } - return out -} diff --git a/services/room-service/internal/room/service/decoration.go b/services/room-service/internal/room/service/decoration.go new file mode 100644 index 00000000..1e21b7ef --- /dev/null +++ b/services/room-service/internal/room/service/decoration.go @@ -0,0 +1,290 @@ +package service + +import ( + "context" + "encoding/json" + "math" + "reflect" + "regexp" + "strconv" + "strings" + "time" + + roomeventsv1 "hyapp.local/api/proto/events/room/v1" + roomv1 "hyapp.local/api/proto/room/v1" + walletv1 "hyapp.local/api/proto/wallet/v1" + "hyapp/pkg/appcode" + "hyapp/pkg/xerr" + "hyapp/services/room-service/internal/integration" + "hyapp/services/room-service/internal/room/command" + "hyapp/services/room-service/internal/room/outbox" + "hyapp/services/room-service/internal/room/rank" + "hyapp/services/room-service/internal/room/state" +) + +const ( + roomDecorationTypeBorder = "room_border" + roomDecorationTypeColoredName = "colored_room_name" + roomDecorationResourceBorder = "room_border" + roomDecorationResourceName = "room_name_style" +) + +var roomDecorationColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$`) + +type roomDecorationMetadata struct { + Format string `json:"format"` + TextColors []string `json:"text_colors"` + TextColorsLegacy []string `json:"textColors"` + Stops []float64 `json:"stops"` + AngleDegrees *float64 `json:"angle_degrees"` + AngleLegacy *float64 `json:"angleDegrees"` +} + +// ApplyRoomDecoration 把 wallet 当前仍授权的资源快照串行写入 Room Cell。 +// entitlement 与 VIP benefit 都必须精确绑定同一 resource,防止会员降级后复用历史高等级样式。 +func (s *Service) ApplyRoomDecoration(ctx context.Context, req *roomv1.ApplyRoomDecorationRequest) (*roomv1.ApplyRoomDecorationResponse, error) { + ctx = contextFromMeta(ctx, req.GetMeta()) + decorationType, resourceType, benefitCode, err := normalizeRoomDecorationRequest(req) + if err != nil { + return nil, err + } + cmd := command.ApplyRoomDecoration{ + Base: baseFromMeta(req.GetMeta()), DecorationType: decorationType, + ResourceID: req.GetResourceId(), EntitlementID: strings.TrimSpace(req.GetEntitlementId()), + } + // 已提交命令必须先于 wallet 当前态重放;否则 VIP/entitlement 后续到期会把合法重试改成失败。 + if _, record, seen, checkErr := s.commandRecordForIdempotency(ctx, cmd); checkErr != nil { + return nil, checkErr + } else if seen { + stored, deserializeErr := command.Deserialize(record.CommandType, record.Payload) + if deserializeErr != nil { + return nil, deserializeErr + } + storedCommand, ok := stored.(*command.ApplyRoomDecoration) + if !ok { + return nil, xerr.New(xerr.Internal, "stored room decoration command is invalid") + } + cmd = *storedCommand + } else { + if err := s.requireRoomOwnerMeta(ctx, cmd.RoomID(), cmd.ActorUserID()); err != nil { + return nil, err + } + resource, resolveErr := s.resolveRoomDecoration(ctx, req.GetMeta().GetRequestId(), cmd.ActorUserID(), cmd.ResourceID, cmd.EntitlementID, resourceType, benefitCode) + if resolveErr != nil { + return nil, resolveErr + } + cmd.Resource = resource + } + + result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { + if err := requireActiveRoom(current); err != nil { + return mutationResult{}, nil, err + } + if current.OwnerUserID != cmd.ActorUserID() { + return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "permission denied") + } + if cmd.Resource.ResourceID <= 0 || (cmd.Resource.ExpiresAtMS > 0 && now.UnixMilli() >= cmd.Resource.ExpiresAtMS) { + return mutationResult{}, nil, xerr.NewWithMetadata(xerr.VIPBenefitRequired, "room decoration entitlement expired", map[string]string{ + "benefit_code": benefitCode, "required_level": "0", "current_effective_level": "0", "action": "apply_room_decoration", + }) + } + var currentDecoration **state.RoomDecoration + switch cmd.DecorationType { + case roomDecorationTypeBorder: + currentDecoration = ¤t.RoomBorder + case roomDecorationTypeColoredName: + currentDecoration = ¤t.RoomNameStyle + default: + return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "decoration_type is invalid") + } + assignmentChange := roomDecorationAssignmentChange(current, cmd.DecorationType, benefitCode, cmd.Resource, now.UnixMilli()) + if *currentDecoration != nil && reflect.DeepEqual(**currentDecoration, cmd.Resource) { + return mutationResult{snapshot: current.ToProtoAt(now.UnixMilli()), roomDecorationAssignment: assignmentChange}, nil, nil + } + decoration := cmd.Resource + *currentDecoration = &decoration + current.Version++ + event, err := outbox.Build(current.RoomID, "RoomDecorationChanged", current.Version, now, &roomeventsv1.RoomDecorationChanged{ + ActorUserId: cmd.ActorUserID(), DecorationType: cmd.DecorationType, ResourceId: decoration.ResourceID, + ResourceCode: decoration.ResourceCode, ResourceType: decoration.ResourceType, Name: decoration.Name, + AssetUrl: decoration.AssetURL, PreviewUrl: decoration.PreviewURL, AnimationUrl: decoration.AnimationURL, + Format: decoration.Format, MetadataJson: decoration.MetadataJSON, TextColors: append([]string(nil), decoration.TextColors...), + Stops: append([]float64(nil), decoration.Stops...), AngleDegrees: decoration.AngleDegrees, + EntitlementId: decoration.EntitlementID, ExpiresAtMs: decoration.ExpiresAtMS, + }) + if err != nil { + return mutationResult{}, nil, err + } + return mutationResult{snapshot: current.ToProtoAt(now.UnixMilli()), roomDecorationAssignment: assignmentChange}, []outbox.Record{event}, nil + }) + if err != nil { + return nil, err + } + resource := roomDecorationToProto(&cmd.Resource, s.clock.Now().UnixMilli()) + return &roomv1.ApplyRoomDecorationResponse{ + Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()), Room: result.snapshot, + DecorationType: decorationType, Resource: resource, ServerTimeMs: s.clock.Now().UnixMilli(), + }, nil +} + +func roomDecorationAssignmentChange(current *state.RoomState, decorationType string, benefitCode string, resource state.RoomDecoration, nowMS int64) *RoomDecorationAssignmentChange { + assignment := &RoomDecorationAssignment{ + // AppCode 由 SaveMutation 使用当前请求上下文写入,避免把租户字段复制进 RoomState。 + RoomID: current.RoomID, DecorationType: decorationType, OwnerUserID: current.OwnerUserID, + BenefitCode: benefitCode, ResourceID: resource.ResourceID, EntitlementID: resource.EntitlementID, + ExpiresAtMS: resource.ExpiresAtMS, UpdatedAtMS: nowMS, + } + return &RoomDecorationAssignmentChange{DecorationType: decorationType, Assignment: assignment} +} + +func normalizeRoomDecorationRequest(req *roomv1.ApplyRoomDecorationRequest) (string, string, string, error) { + if req == nil || req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetCommandId()) == "" || req.GetResourceId() <= 0 { + return "", "", "", xerr.New(xerr.InvalidArgument, "room decoration request is incomplete") + } + switch normalizeVIPCode(req.GetDecorationType()) { + case roomDecorationTypeBorder: + return roomDecorationTypeBorder, roomDecorationResourceBorder, vipBenefitRoomBorder, nil + case roomDecorationTypeColoredName: + return roomDecorationTypeColoredName, roomDecorationResourceName, vipBenefitColoredRoomName, nil + default: + return "", "", "", xerr.New(xerr.InvalidArgument, "decoration_type is invalid") + } +} + +func (s *Service) resolveRoomDecoration(ctx context.Context, requestID string, userID int64, resourceID int64, entitlementID string, resourceType string, benefitCode string) (state.RoomDecoration, error) { + checker, ok := s.wallet.(integration.WalletVIPBenefitClient) + if !ok { + return state.RoomDecoration{}, xerr.New(xerr.Unavailable, "wallet vip benefit client is unavailable") + } + lookupCtx, cancel := context.WithTimeout(ctx, roomVIPLookupTimeout) + defer cancel() + decision, err := checker.CheckVipBenefit(lookupCtx, &walletv1.CheckVipBenefitRequest{ + RequestId: strings.TrimSpace(requestID), AppCode: appcode.FromContext(ctx), UserId: userID, BenefitCode: benefitCode, + }) + if err != nil { + return state.RoomDecoration{}, xerr.New(xerr.Unavailable, "check room decoration vip benefit failed") + } + benefit := decision.GetBenefit() + catalogResource := benefit.GetResource() + if decision == nil || !decision.GetAllowed() || benefit == nil || benefit.GetResourceId() != resourceID || + normalizeVIPCode(benefit.GetResourceType()) != resourceType || catalogResource == nil || + catalogResource.GetResourceId() != resourceID || normalizeVIPCode(catalogResource.GetResourceType()) != resourceType || + normalizeVIPCode(catalogResource.GetStatus()) != "active" { + requiredLevel, currentLevel := int32(0), int32(0) + if decision != nil { + requiredLevel, currentLevel = decision.GetRequiredLevel(), decision.GetCurrentEffectiveLevel() + } + return state.RoomDecoration{}, xerr.NewWithMetadata(xerr.VIPBenefitRequired, "room decoration vip benefit is required", map[string]string{ + "benefit_code": benefitCode, "required_level": strconv.FormatInt(int64(requiredLevel), 10), + "current_effective_level": strconv.FormatInt(int64(currentLevel), 10), "action": "apply_room_decoration", + }) + } + resources, ok := s.wallet.(integration.WalletUserResourceClient) + if !ok { + return state.RoomDecoration{}, xerr.New(xerr.Unavailable, "wallet user resource client is unavailable") + } + resourceCtx, resourceCancel := context.WithTimeout(ctx, roomVIPLookupTimeout) + defer resourceCancel() + resourceResponse, err := resources.ListUserResources(resourceCtx, &walletv1.ListUserResourcesRequest{ + RequestId: strings.TrimSpace(requestID), AppCode: appcode.FromContext(ctx), UserId: userID, + ResourceType: resourceType, ActiveOnly: true, + }) + if err != nil { + return state.RoomDecoration{}, xerr.New(xerr.Unavailable, "list room decoration resources failed") + } + nowMS := s.clock.Now().UnixMilli() + for _, entitlement := range resourceResponse.GetResources() { + entitlementResource := entitlement.GetResource() + if entitlement.GetResourceId() != resourceID || entitlementResource == nil || entitlementResource.GetResourceId() != resourceID || + normalizeVIPCode(entitlementResource.GetResourceType()) != resourceType { + continue + } + if entitlementID != "" && strings.TrimSpace(entitlement.GetEntitlementId()) != entitlementID { + continue + } + if normalizeVIPCode(entitlement.GetStatus()) != "active" || entitlement.GetEffectiveAtMs() > nowMS || + (entitlement.GetExpiresAtMs() > 0 && entitlement.GetExpiresAtMs() <= nowMS) || entitlement.GetRemainingQuantity() <= 0 { + continue + } + // entitlement 只证明该用户仍有权使用 resource_id;展示内容必须取 CheckVipBenefit 返回的 + // 当前 active 目录资源。这样后台更新素材后会刷新房间,且不会继续渲染 entitlement 的历史快照。 + metadata, metadataErr := parseRoomDecorationMetadata(resourceType, catalogResource.GetMetadataJson()) + if metadataErr != nil { + return state.RoomDecoration{}, metadataErr + } + vipExpiresAtMS := int64(0) + if decision.GetState() != nil && decision.GetState().GetEffectiveVip() != nil { + vipExpiresAtMS = decision.GetState().GetEffectiveVip().GetExpiresAtMs() + } + angleDegrees := float64(0) + if metadata.AngleDegrees != nil { + angleDegrees = *metadata.AngleDegrees + } + return state.RoomDecoration{ + ResourceID: catalogResource.ResourceId, ResourceCode: catalogResource.ResourceCode, ResourceType: catalogResource.ResourceType, + Name: catalogResource.Name, AssetURL: catalogResource.AssetUrl, PreviewURL: catalogResource.PreviewUrl, AnimationURL: catalogResource.AnimationUrl, + Format: metadata.Format, MetadataJSON: catalogResource.MetadataJson, EntitlementID: entitlement.EntitlementId, + TextColors: metadata.TextColors, Stops: metadata.Stops, AngleDegrees: angleDegrees, + ExpiresAtMS: minNonZeroMS(entitlement.GetExpiresAtMs(), vipExpiresAtMS), + }, nil + } + return state.RoomDecoration{}, xerr.New(xerr.NotFound, "active room decoration entitlement not found") +} + +func parseRoomDecorationMetadata(resourceType string, raw string) (roomDecorationMetadata, error) { + var metadata roomDecorationMetadata + if strings.TrimSpace(raw) != "" && json.Unmarshal([]byte(raw), &metadata) != nil { + return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room decoration metadata_json is invalid") + } + if len(metadata.TextColors) == 0 { + metadata.TextColors = metadata.TextColorsLegacy + } + if metadata.AngleDegrees == nil { + metadata.AngleDegrees = metadata.AngleLegacy + } + metadata.Format = strings.ToLower(strings.TrimSpace(metadata.Format)) + if metadata.AngleDegrees != nil && (math.IsNaN(*metadata.AngleDegrees) || math.IsInf(*metadata.AngleDegrees, 0)) { + return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room decoration angle is invalid") + } + if resourceType == roomDecorationResourceName { + if len(metadata.TextColors) == 0 || len(metadata.TextColors) > 8 || (len(metadata.Stops) != 0 && len(metadata.Stops) != len(metadata.TextColors)) { + return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room name style colors are invalid") + } + for _, color := range metadata.TextColors { + if !roomDecorationColorPattern.MatchString(strings.TrimSpace(color)) { + return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room name style color is invalid") + } + } + previous := -1.0 + for _, stop := range metadata.Stops { + if math.IsNaN(stop) || math.IsInf(stop, 0) || stop < 0 || stop > 1 || stop < previous { + return roomDecorationMetadata{}, xerr.New(xerr.InvalidArgument, "room name style stops are invalid") + } + previous = stop + } + } + return metadata, nil +} + +func minNonZeroMS(left int64, right int64) int64 { + if left <= 0 { + return right + } + if right <= 0 || left < right { + return left + } + return right +} + +func roomDecorationToProto(resource *state.RoomDecoration, nowMS int64) *roomv1.RoomDecorationResource { + if resource == nil || resource.ResourceID <= 0 || (resource.ExpiresAtMS > 0 && nowMS >= resource.ExpiresAtMS) { + return nil + } + return &roomv1.RoomDecorationResource{ + ResourceId: resource.ResourceID, ResourceCode: resource.ResourceCode, ResourceType: resource.ResourceType, + Name: resource.Name, AssetUrl: resource.AssetURL, PreviewUrl: resource.PreviewURL, AnimationUrl: resource.AnimationURL, + Format: resource.Format, MetadataJson: resource.MetadataJSON, EntitlementId: resource.EntitlementID, + TextColors: append([]string(nil), resource.TextColors...), Stops: append([]float64(nil), resource.Stops...), + AngleDegrees: resource.AngleDegrees, ExpiresAtMs: resource.ExpiresAtMS, + } +} diff --git a/services/room-service/internal/room/service/image_message.go b/services/room-service/internal/room/service/image_message.go new file mode 100644 index 00000000..7d9166d6 --- /dev/null +++ b/services/room-service/internal/room/service/image_message.go @@ -0,0 +1,332 @@ +package service + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "strings" + "time" + + roomeventsv1 "hyapp.local/api/proto/events/room/v1" + roomv1 "hyapp.local/api/proto/room/v1" + "hyapp/pkg/appcode" + "hyapp/pkg/giftlimits" + "hyapp/pkg/roomid" + "hyapp/pkg/xerr" + "hyapp/services/room-service/internal/room/command" + "hyapp/services/room-service/internal/room/outbox" + "hyapp/services/room-service/internal/room/rank" + "hyapp/services/room-service/internal/room/state" +) + +// AuthorizeRoomMediaUpload 在 COS PutObject 之前执行房间 owner/presence、治理态和 VIP 权益门禁。 +// 返回的 prefix 是 gateway 生成对象 key 的唯一可信目录,客户端不能自行指定 key。 +func (s *Service) AuthorizeRoomMediaUpload(ctx context.Context, req *roomv1.AuthorizeRoomMediaUploadRequest) (*roomv1.AuthorizeRoomMediaUploadResponse, error) { + if req == nil || req.GetMeta() == nil { + return nil, xerr.New(xerr.InvalidArgument, "request is required") + } + ctx = contextFromMeta(ctx, req.GetMeta()) + roomID := strings.TrimSpace(req.GetMeta().GetRoomId()) + actorUserID := req.GetMeta().GetActorUserId() + purpose := strings.ToLower(strings.TrimSpace(req.GetPurpose())) + if !roomid.ValidStringID(roomID) || actorUserID <= 0 || !giftlimits.ValidCommandID(req.GetMeta().GetCommandId()) || + (purpose != roomMediaPurposeBackground && purpose != roomMediaPurposeImage) { + return nil, xerr.New(xerr.InvalidArgument, "room media upload request is invalid") + } + media := roomMediaFromProto(req.GetMedia()) + if err := validateRoomMediaProperties(purpose, media); err != nil { + return nil, err + } + if media.URL != "" || media.ObjectKey != "" || media.Status != roomMediaStatusActive { + return nil, xerr.New(xerr.InvalidArgument, "room media authorization requires pre-upload metadata") + } + guard, ok := s.repository.(RoomMediaUploadGuardStore) + if !ok { + return nil, xerr.New(xerr.Unavailable, "room media upload guard is unavailable") + } + payloadSHA256, err := roomMediaUploadPayloadSHA256(media) + if err != nil { + return nil, err + } + registration := RoomMediaUploadRegistration{ + AppCode: appcode.FromContext(ctx), RoomID: roomID, CommandID: strings.TrimSpace(req.GetMeta().GetCommandId()), + ActorUserID: actorUserID, Purpose: purpose, MediaPayloadSHA256: payloadSHA256, Media: media, + } + registration.ExpectedObjectKey = roomMediaExpectedObjectKey( + registration.AppCode, actorUserID, roomID, purpose, registration.CommandID, media, + ) + var authorizedReplay *RoomMediaUploadRegistration + if existing, exists, readErr := guard.GetRoomMediaUploadRegistration(ctx, roomID, registration.CommandID); readErr != nil { + return nil, readErr + } else if exists { + if !sameRoomMediaUploadIdentity(existing, registration) { + return nil, xerr.New(xerr.IdempotencyConflict, "room media upload command payload conflict") + } + switch existing.Status { + case roomMediaStatusActive: + // PutObject 已完成的同载荷重试只重放首次结果;后续权限变化不能把已成功请求改成失败。 + return roomMediaUploadAuthorizationResponse(existing, s.clock.Now().UnixMilli()), nil + case "authorized": + // authorized 只代表 owner 已占用 command_id,不能代表对象已经上传成功。重试 PutObject + // 前必须重新检查当前房主/麦位/VIP,避免权限撤销后继续使用旧授权写 COS。 + authorizedReplay = &existing + default: + return nil, xerr.New(xerr.Internal, "room media upload registration status is invalid") + } + } + snapshot, err := s.currentSnapshot(ctx, roomID) + if err != nil { + return nil, err + } + if snapshot == nil || snapshot.GetStatus() != state.RoomStatusActive { + return nil, xerr.New(xerr.RoomClosed, "room closed") + } + switch purpose { + case roomMediaPurposeBackground: + if snapshot.GetOwnerUserId() != actorUserID { + return nil, xerr.New(xerr.PermissionDenied, "permission denied") + } + if err := s.requireVIPBenefit(ctx, req.GetMeta().GetRequestId(), actorUserID, vipBenefitCustomRoomBackground, "upload_room_background"); err != nil { + return nil, err + } + case roomMediaPurposeImage: + if err := requireSnapshotSpeakPermission(snapshot, actorUserID, s.clock.Now().UnixMilli()); err != nil { + return nil, err + } + if err := s.requireVIPBenefit(ctx, req.GetMeta().GetRequestId(), actorUserID, vipBenefitRoomImageMessage, "upload_room_image"); err != nil { + return nil, err + } + } + nowMS := s.clock.Now().UnixMilli() + if authorizedReplay != nil { + return roomMediaUploadAuthorizationResponse(*authorizedReplay, nowMS), nil + } + registration.AuthorizedVersion = snapshot.GetVersion() + registration.CreatedAtMS, registration.UpdatedAtMS = nowMS, nowMS + registered, err := guard.RegisterRoomMediaUpload(ctx, registration) + if errors.Is(err, ErrRoomMediaUploadCommandConflict) { + return nil, xerr.New(xerr.IdempotencyConflict, "room media upload command payload conflict") + } + if err != nil { + return nil, err + } + return roomMediaUploadAuthorizationResponse(registered, nowMS), nil +} + +// CompleteRoomMediaUpload 只接受 gateway 已成功 PutObject 后回传的 URL/key。 +// owner 以首次注册的完整解析事实为准推进 active,Save/Send 随后只认 active registration。 +func (s *Service) CompleteRoomMediaUpload(ctx context.Context, req *roomv1.CompleteRoomMediaUploadRequest) (*roomv1.CompleteRoomMediaUploadResponse, error) { + if req == nil || req.GetMeta() == nil { + return nil, xerr.New(xerr.InvalidArgument, "request is required") + } + ctx = contextFromMeta(ctx, req.GetMeta()) + roomID := strings.TrimSpace(req.GetMeta().GetRoomId()) + commandID := strings.TrimSpace(req.GetMeta().GetCommandId()) + actorUserID := req.GetMeta().GetActorUserId() + purpose := strings.ToLower(strings.TrimSpace(req.GetPurpose())) + if !roomid.ValidStringID(roomID) || actorUserID <= 0 || !giftlimits.ValidCommandID(commandID) || + (purpose != roomMediaPurposeBackground && purpose != roomMediaPurposeImage) { + return nil, xerr.New(xerr.InvalidArgument, "room media completion request is invalid") + } + media := roomMediaFromProto(req.GetMedia()) + if err := validateRoomMedia(appcode.FromContext(ctx), roomID, actorUserID, purpose, media); err != nil { + return nil, err + } + guard, ok := s.repository.(RoomMediaUploadGuardStore) + if !ok { + return nil, xerr.New(xerr.Unavailable, "room media upload guard is unavailable") + } + existing, exists, err := guard.GetRoomMediaUploadRegistration(ctx, roomID, commandID) + if err != nil { + return nil, err + } + if !exists { + return nil, xerr.New(xerr.NotFound, "room media upload authorization not found") + } + payloadSHA256, err := roomMediaUploadPayloadSHA256(media) + if err != nil { + return nil, err + } + candidate := RoomMediaUploadRegistration{ + AppCode: appcode.FromContext(ctx), RoomID: roomID, CommandID: commandID, ActorUserID: actorUserID, + Purpose: purpose, MediaPayloadSHA256: payloadSHA256, ExpectedObjectKey: media.ObjectKey, ObjectURL: media.URL, Media: media, + } + if !sameRoomMediaUploadIdentity(existing, candidate) || !sameRoomMediaProperties(existing.Media, media) { + return nil, xerr.New(xerr.IdempotencyConflict, "room media upload command payload conflict") + } + if existing.Status == roomMediaStatusActive { + if strings.TrimSpace(existing.ObjectURL) != strings.TrimSpace(media.URL) { + return nil, xerr.New(xerr.IdempotencyConflict, "room media upload completion conflict") + } + return &roomv1.CompleteRoomMediaUploadResponse{ + Active: true, Media: roomMediaToProto(existing.Media), ServerTimeMs: s.clock.Now().UnixMilli(), + }, nil + } + existing.ObjectURL, existing.Status, existing.Media = media.URL, roomMediaStatusActive, media + existing.UpdatedAtMS = s.clock.Now().UnixMilli() + completed, err := guard.CompleteRoomMediaUpload(ctx, existing) + if errors.Is(err, ErrRoomMediaUploadCommandConflict) { + return nil, xerr.New(xerr.IdempotencyConflict, "room media upload completion conflict") + } + if err != nil { + return nil, err + } + return &roomv1.CompleteRoomMediaUploadResponse{ + Active: true, Media: roomMediaToProto(completed.Media), ServerTimeMs: s.clock.Now().UnixMilli(), + }, nil +} + +func roomMediaUploadAuthorizationResponse(registration RoomMediaUploadRegistration, serverTimeMS int64) *roomv1.AuthorizeRoomMediaUploadResponse { + return &roomv1.AuthorizeRoomMediaUploadResponse{ + Allowed: true, ObjectKeyPrefix: roomMediaObjectPrefix(registration.AppCode, registration.ActorUserID, registration.RoomID, registration.Purpose), + ObjectKey: registration.ExpectedObjectKey, RoomVersion: registration.AuthorizedVersion, ServerTimeMs: serverTimeMS, + } +} + +func sameRoomMediaUploadIdentity(left RoomMediaUploadRegistration, right RoomMediaUploadRegistration) bool { + return appcode.Normalize(left.AppCode) == appcode.Normalize(right.AppCode) && + strings.TrimSpace(left.RoomID) == strings.TrimSpace(right.RoomID) && + strings.TrimSpace(left.CommandID) == strings.TrimSpace(right.CommandID) && + left.ActorUserID == right.ActorUserID && strings.TrimSpace(left.Purpose) == strings.TrimSpace(right.Purpose) && + strings.EqualFold(strings.TrimSpace(left.MediaPayloadSHA256), strings.TrimSpace(right.MediaPayloadSHA256)) && + strings.TrimLeft(strings.TrimSpace(left.ExpectedObjectKey), "/") == strings.TrimLeft(strings.TrimSpace(right.ExpectedObjectKey), "/") +} + +func (s *Service) requireActiveRoomMediaUpload(ctx context.Context, roomID string, actorUserID int64, purpose string, commandID string, media state.RoomMedia) error { + guard, ok := s.repository.(RoomMediaUploadGuardStore) + if !ok { + return xerr.New(xerr.Unavailable, "room media upload guard is unavailable") + } + registration, exists, err := guard.GetActiveRoomMediaUploadByObjectKey(ctx, media.ObjectKey) + if err != nil { + return err + } + if !exists { + return xerr.New(xerr.NotFound, "active room media upload not found") + } + payloadSHA256, err := roomMediaUploadPayloadSHA256(media) + if err != nil { + return err + } + candidate := RoomMediaUploadRegistration{ + AppCode: appcode.FromContext(ctx), RoomID: roomID, CommandID: strings.TrimSpace(commandID), ActorUserID: actorUserID, + Purpose: purpose, MediaPayloadSHA256: payloadSHA256, ExpectedObjectKey: media.ObjectKey, ObjectURL: media.URL, Media: media, + } + if !sameRoomMediaUploadIdentity(registration, candidate) || + strings.TrimSpace(registration.ObjectURL) != strings.TrimSpace(media.URL) || !sameRoomMediaProperties(registration.Media, media) { + return xerr.New(xerr.IdempotencyConflict, "room media does not match completed upload") + } + return nil +} + +// SendRoomImage 把服务端认可的图片消息写入 Room Cell command log + durable outbox。 +// outbox 的 at-least-once 重试使用稳定 message_id,Flutter 不能以到达次数作为消息条数。 +func (s *Service) SendRoomImage(ctx context.Context, req *roomv1.SendRoomImageRequest) (*roomv1.SendRoomImageResponse, error) { + if req == nil || req.GetMeta() == nil { + return nil, xerr.New(xerr.InvalidArgument, "request is required") + } + ctx = contextFromMeta(ctx, req.GetMeta()) + cmd := command.SendRoomImage{ + Base: baseFromMeta(req.GetMeta()), MessageID: roomImageMessageID(req.GetMeta()), Image: roomMediaFromProto(req.GetImage()), + } + if !roomid.ValidStringID(cmd.RoomID()) || cmd.ActorUserID() <= 0 || strings.TrimSpace(cmd.ID()) == "" { + return nil, xerr.New(xerr.InvalidArgument, "room image message meta is invalid") + } + // 与装扮相同,已提交 command_id 必须在当前 VIP 到期/禁言之前直接重放。 + if _, record, seen, err := s.commandRecordForIdempotency(ctx, cmd); err != nil { + return nil, err + } else if seen { + stored, deserializeErr := command.Deserialize(record.CommandType, record.Payload) + if deserializeErr != nil { + return nil, deserializeErr + } + storedCommand, ok := stored.(*command.SendRoomImage) + if !ok { + return nil, xerr.New(xerr.Internal, "stored room image command is invalid") + } + // 返回第一次提交固化的 message_id 和媒体事实,不能用当前重试请求拼出看似成功的另一条消息。 + cmd = *storedCommand + } else { + if err := validateRoomMedia(appcode.FromContext(ctx), cmd.RoomID(), cmd.ActorUserID(), roomMediaPurposeImage, cmd.Image); err != nil { + return nil, err + } + if err := s.requireActiveRoomMediaUpload(ctx, cmd.RoomID(), cmd.ActorUserID(), roomMediaPurposeImage, cmd.ID(), cmd.Image); err != nil { + return nil, err + } + if err := s.requireVIPBenefit(ctx, req.GetMeta().GetRequestId(), cmd.ActorUserID(), vipBenefitRoomImageMessage, "send_room_image"); err != nil { + return nil, err + } + } + + result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { + if err := requireActiveRoom(current); err != nil { + return mutationResult{}, nil, err + } + if err := requireStateSpeakPermission(current, cmd.ActorUserID(), now.UnixMilli()); err != nil { + return mutationResult{}, nil, err + } + // 图片消息不成为房间核心状态字段,但版本推进把 command log、恢复谱系和 outbox 放进同一原子提交。 + current.Version++ + event, err := outbox.Build(current.RoomID, "RoomImageMessageSent", current.Version, now, &roomeventsv1.RoomImageMessageSent{ + MessageId: cmd.MessageID, CommandId: cmd.ID(), SenderUserId: cmd.ActorUserID(), Image: roomMediaEventSnapshot(cmd.Image), + }) + if err != nil { + return mutationResult{}, nil, err + } + return mutationResult{snapshot: current.ToProtoAt(now.UnixMilli())}, []outbox.Record{event}, nil + }) + if err != nil { + return nil, err + } + now := s.clock.Now() + return &roomv1.SendRoomImageResponse{ + Result: commandResult(result.applied, result.snapshot.GetVersion(), now), Accepted: true, + MessageId: cmd.MessageID, RoomId: cmd.RoomID(), SenderUserId: cmd.ActorUserID(), ServerTimeMs: now.UnixMilli(), + Image: roomMediaToProto(cmd.Image), + }, nil +} + +func requireSnapshotSpeakPermission(snapshot *roomv1.RoomSnapshot, userID int64, nowMS int64) error { + if snapshot == nil || snapshot.GetStatus() != state.RoomStatusActive { + return xerr.New(xerr.RoomClosed, "room closed") + } + switch { + case snapshotUserBanned(snapshot, userID, nowMS): + return xerr.New(xerr.PermissionDenied, "user is banned") + case containsID(snapshot.GetMuteUserIds(), userID): + return xerr.New(xerr.PermissionDenied, "user is muted") + case !snapshot.GetChatEnabled(): + return xerr.New(xerr.Conflict, "room chat is disabled") + case findProtoUser(snapshot, userID) == nil: + return xerr.New(xerr.PermissionDenied, "user is not in room") + default: + return nil + } +} + +func requireStateSpeakPermission(current *state.RoomState, userID int64, nowMS int64) error { + if moderation, banned := current.BanUsers[userID]; banned && moderation.ActiveAt(nowMS) { + return xerr.New(xerr.PermissionDenied, "user is banned") + } + if current.MuteUsers[userID] { + return xerr.New(xerr.PermissionDenied, "user is muted") + } + if !current.ChatEnabled { + return xerr.New(xerr.Conflict, "room chat is disabled") + } + if current.OnlineUsers[userID] == nil { + return xerr.New(xerr.PermissionDenied, "user is not in room") + } + return nil +} + +func roomImageMessageID(meta *roomv1.RequestMeta) string { + if meta == nil { + return "" + } + sum := sha256.Sum256([]byte(strings.Join([]string{ + appcode.Normalize(meta.GetAppCode()), strings.TrimSpace(meta.GetRoomId()), strings.TrimSpace(meta.GetCommandId()), + }, "\x00"))) + return "room_msg_" + hex.EncodeToString(sum[:16]) +} diff --git a/services/room-service/internal/room/service/list.go b/services/room-service/internal/room/service/list.go index 57239330..8229b1e1 100644 --- a/services/room-service/internal/room/service/list.go +++ b/services/room-service/internal/room/service/list.go @@ -7,6 +7,7 @@ import ( "log/slog" "math" "strings" + "time" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" @@ -403,6 +404,8 @@ func roomListEntryFromSnapshot(snapshot *roomv1.RoomSnapshot, visibleRegionID in SeatCount: seatCount, OccupiedSeatCount: occupiedSeatCount, SortScore: roomListSortScore(snapshot.GetHeat(), onlineCount, occupiedSeatCount), + RoomBorderJSON: marshalRoomListDecoration(snapshot.GetRoomBorder()), + RoomNameStyleJSON: marshalRoomListDecoration(snapshot.GetRoomNameStyle()), CreatedAtMS: nowMS, UpdatedAtMS: nowMS, } @@ -587,6 +590,7 @@ func encodeRoomListCursor(tab string, query string, countryCode string, viewerCo // roomListItemToProto 将 repository 投影转换为跨服务响应对象。 func roomListItemToProto(entry RoomListEntry) *roomv1.RoomListItem { + nowMS := time.Now().UTC().UnixMilli() return &roomv1.RoomListItem{ AppCode: entry.AppCode, RoomId: entry.RoomID, @@ -603,9 +607,34 @@ func roomListItemToProto(entry RoomListEntry) *roomv1.RoomListItem { OccupiedSeatCount: entry.OccupiedSeatCount, VisibleRegionId: entry.VisibleRegionID, CountryCode: normalizeRoomCountryCode(entry.OwnerCountryCode), + RoomBorder: unmarshalRoomListDecoration(entry.RoomBorderJSON, nowMS), + RoomNameStyle: unmarshalRoomListDecoration(entry.RoomNameStyleJSON, nowMS), } } +func marshalRoomListDecoration(resource *roomv1.RoomDecorationResource) string { + if resource == nil || resource.GetResourceId() <= 0 { + return "" + } + payload, err := json.Marshal(resource) + if err != nil { + return "" + } + return string(payload) +} + +func unmarshalRoomListDecoration(raw string, nowMS int64) *roomv1.RoomDecorationResource { + if strings.TrimSpace(raw) == "" { + return nil + } + var resource roomv1.RoomDecorationResource + if err := json.Unmarshal([]byte(raw), &resource); err != nil || resource.GetResourceId() <= 0 || + (resource.GetExpiresAtMs() > 0 && nowMS >= resource.GetExpiresAtMs()) { + return nil + } + return &resource +} + func roomListEntryFromMetaAndSnapshot(meta RoomMeta, snapshot *roomv1.RoomSnapshot, nowMS int64) RoomListEntry { if snapshot == nil || snapshot.GetRoomId() == "" { // GetMyRoom 兜底路径:即使快照暂时不可用,也能用 rooms 元数据返回“有房间”的最小卡片。 diff --git a/services/room-service/internal/room/service/media.go b/services/room-service/internal/room/service/media.go new file mode 100644 index 00000000..b14040d8 --- /dev/null +++ b/services/room-service/internal/room/service/media.go @@ -0,0 +1,248 @@ +package service + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "net/url" + "path" + "strings" + + roomeventsv1 "hyapp.local/api/proto/events/room/v1" + roomv1 "hyapp.local/api/proto/room/v1" + "hyapp/pkg/appcode" + "hyapp/pkg/xerr" + "hyapp/services/room-service/internal/room/state" +) + +const ( + roomMediaPurposeBackground = "room_background" + roomMediaPurposeImage = "room_image" + roomMediaStatusActive = "active" + + roomBackgroundMaxBytes = int64(12 << 20) + roomBackgroundMaxWidth = int32(2160) + roomBackgroundMaxHeight = int32(3840) + roomBackgroundMaxPixels = int64(8_294_400) + roomImageMaxBytes = int64(8 << 20) + roomImageMaxDimension = int32(4096) + roomImageMaxPixels = int64(16_777_216) + roomMediaMaxFrames = int32(120) + roomMediaMaxDurationMS = int64(15_000) +) + +var roomMediaMIMEByFormat = map[string]string{ + "jpeg": "image/jpeg", + "png": "image/png", + "gif": "image/gif", + "webp": "image/webp", +} + +// validateRoomMedia 校验专用上传入口产生的完整媒体事实。room-service 不下载 URL 重新嗅探, +// 但会约束 COS key 归属、https、格式/MIME、尺寸和哈希,防止客户端绕过 gateway 直调 gRPC。 +func validateRoomMedia(ctxAppCode string, roomID string, actorUserID int64, purpose string, media state.RoomMedia) error { + media.URL = strings.TrimSpace(media.URL) + media.ObjectKey = strings.TrimLeft(strings.TrimSpace(media.ObjectKey), "/") + media.ContentType = strings.ToLower(strings.TrimSpace(media.ContentType)) + media.Format = strings.ToLower(strings.TrimSpace(media.Format)) + media.SHA256 = strings.ToLower(strings.TrimSpace(media.SHA256)) + media.Status = strings.ToLower(strings.TrimSpace(media.Status)) + if media.URL == "" || media.ObjectKey == "" || media.Status != roomMediaStatusActive { + return xerr.New(xerr.InvalidArgument, "room media is incomplete") + } + if len(media.URL) > 1024 || len(media.ObjectKey) > 512 || len(media.PosterURL) > 1024 || len(media.ThumbnailURL) > 1024 { + return xerr.New(xerr.InvalidArgument, "room media url or object_key is too long") + } + parsed, err := url.Parse(media.URL) + if err != nil || parsed.Scheme != "https" || strings.TrimSpace(parsed.Host) == "" { + return xerr.New(xerr.InvalidArgument, "room media url must be https") + } + if expected := roomMediaObjectPrefix(ctxAppCode, actorUserID, roomID, purpose); !strings.HasPrefix(media.ObjectKey, expected) { + return xerr.New(xerr.PermissionDenied, "room media object does not belong to actor and room") + } + if path.Clean(media.ObjectKey) != media.ObjectKey || + !(strings.TrimLeft(parsed.Path, "/") == media.ObjectKey || strings.HasSuffix(strings.TrimRight(parsed.Path, "/"), "/"+media.ObjectKey)) { + // gateway 还会按 COS access_url 校验 host/base path;owner service 至少把 URL path 与同一 object_key 绑定, + // 防止客户端拿一条合法 key 配合另一张外部图片绕过素材归属。 + return xerr.New(xerr.PermissionDenied, "room media url does not match object_key") + } + return validateRoomMediaProperties(purpose, media) +} + +// validateRoomMediaProperties 供上传授权阶段在对象尚未写入 COS、因而还没有 URL/key 时复核解析结果。 +func validateRoomMediaProperties(purpose string, media state.RoomMedia) error { + media.ContentType = strings.ToLower(strings.TrimSpace(media.ContentType)) + media.Format = strings.ToLower(strings.TrimSpace(media.Format)) + media.SHA256 = strings.ToLower(strings.TrimSpace(media.SHA256)) + if strings.TrimSpace(media.PosterURL) != "" || strings.TrimSpace(media.ThumbnailURL) != "" { + // 当前上传链路没有生成或校验派生图,客户端不能借 poster/thumbnail 字段挂入任意外部 URL。 + // 等后续服务端真正生成首帧和缩略图后,再由 owner service 填充这两个只读事实字段。 + return xerr.New(xerr.InvalidArgument, "poster_url and thumbnail_url are not supported") + } + expectedMIME, supported := roomMediaMIMEByFormat[media.Format] + if !supported || expectedMIME != media.ContentType { + return xerr.New(xerr.InvalidArgument, "room media format does not match content_type") + } + if media.Width <= 0 || media.Height <= 0 || media.FrameCount <= 0 || media.SizeBytes <= 0 { + return xerr.New(xerr.InvalidArgument, "room media dimensions are invalid") + } + decodedHash, err := hex.DecodeString(media.SHA256) + if err != nil || len(decodedHash) != sha256.Size { + return xerr.New(xerr.InvalidArgument, "room media sha256 is invalid") + } + if media.Animated { + if media.Format != "gif" && media.Format != "webp" { + return xerr.New(xerr.InvalidArgument, "animated room media must be gif or webp") + } + if media.FrameCount < 2 || media.FrameCount > roomMediaMaxFrames || media.DurationMS <= 0 || media.DurationMS > roomMediaMaxDurationMS { + return xerr.New(xerr.InvalidArgument, "animated room media exceeds frame or duration limit") + } + } else if media.FrameCount != 1 || media.DurationMS != 0 { + return xerr.New(xerr.InvalidArgument, "static room media metadata is invalid") + } + pixels := int64(media.Width) * int64(media.Height) + switch purpose { + case roomMediaPurposeBackground: + if media.SizeBytes > roomBackgroundMaxBytes || media.Width > roomBackgroundMaxWidth || media.Height > roomBackgroundMaxHeight || pixels > roomBackgroundMaxPixels { + return xerr.New(xerr.InvalidArgument, "room background exceeds media limits") + } + case roomMediaPurposeImage: + if media.SizeBytes > roomImageMaxBytes || media.Width > roomImageMaxDimension || media.Height > roomImageMaxDimension || pixels > roomImageMaxPixels { + return xerr.New(xerr.InvalidArgument, "room image exceeds media limits") + } + default: + return xerr.New(xerr.InvalidArgument, "room media purpose is invalid") + } + return nil +} + +// roomMediaObjectPrefix 是上传与提交共同使用的租户/用户/房间归属边界。 +// path.Join 同时消除可疑的 ../ 片段,gateway 生成对象 key 时必须使用同一规则。 +func roomMediaObjectPrefix(rawAppCode string, actorUserID int64, roomID string, purpose string) string { + directory := "images" + if purpose == roomMediaPurposeBackground { + directory = "backgrounds" + } + return path.Join("room-media", appcode.Normalize(rawAppCode), directory, fmt.Sprintf("%d", actorUserID), strings.TrimSpace(roomID)) + "/" +} + +func roomMediaExpectedObjectKey(rawAppCode string, actorUserID int64, roomID string, purpose string, commandID string, media state.RoomMedia) string { + commandHash := sha256.Sum256([]byte(strings.TrimSpace(commandID))) + extension := media.Format + if extension == "jpeg" { + extension = "jpg" + } + return path.Join( + roomMediaObjectPrefix(rawAppCode, actorUserID, roomID, purpose), + hex.EncodeToString(commandHash[:8])+"-"+strings.ToLower(strings.TrimSpace(media.SHA256))[:16]+"."+extension, + ) +} + +func roomMediaUploadPayloadSHA256(media state.RoomMedia) (string, error) { + // URL/object_key/status 是上传生命周期字段,不属于首次解析的文件事实;其余字段使用结构体固定顺序编码。 + payload, err := json.Marshal(struct { + ContentType string `json:"content_type"` + Format string `json:"format"` + SizeBytes int64 `json:"size_bytes"` + Width int32 `json:"width"` + Height int32 `json:"height"` + Animated bool `json:"animated"` + FrameCount int32 `json:"frame_count"` + DurationMS int64 `json:"duration_ms"` + PosterURL string `json:"poster_url"` + ThumbnailURL string `json:"thumbnail_url"` + SHA256 string `json:"sha256"` + }{ + ContentType: media.ContentType, Format: media.Format, SizeBytes: media.SizeBytes, + Width: media.Width, Height: media.Height, Animated: media.Animated, FrameCount: media.FrameCount, + DurationMS: media.DurationMS, PosterURL: media.PosterURL, ThumbnailURL: media.ThumbnailURL, SHA256: media.SHA256, + }) + if err != nil { + return "", err + } + sum := sha256.Sum256(payload) + return hex.EncodeToString(sum[:]), nil +} + +func sameRoomMediaProperties(left state.RoomMedia, right state.RoomMedia) bool { + left.URL, left.ObjectKey, left.Status = "", "", "" + right.URL, right.ObjectKey, right.Status = "", "", "" + return left == right +} + +func roomMediaFromProto(media *roomv1.RoomMedia) state.RoomMedia { + if media == nil { + return state.RoomMedia{} + } + return state.RoomMedia{ + URL: strings.TrimSpace(media.GetUrl()), + ObjectKey: strings.TrimLeft(strings.TrimSpace(media.GetObjectKey()), "/"), + ContentType: strings.ToLower(strings.TrimSpace(media.GetContentType())), + Format: strings.ToLower(strings.TrimSpace(media.GetFormat())), + SizeBytes: media.GetSizeBytes(), + Width: media.GetWidth(), + Height: media.GetHeight(), + Animated: media.GetAnimated(), + FrameCount: media.GetFrameCount(), + DurationMS: media.GetDurationMs(), + PosterURL: strings.TrimSpace(media.GetPosterUrl()), + ThumbnailURL: strings.TrimSpace(media.GetThumbnailUrl()), + SHA256: strings.ToLower(strings.TrimSpace(media.GetSha256())), + Status: strings.ToLower(strings.TrimSpace(media.GetStatus())), + } +} + +func roomMediaToProto(media state.RoomMedia) *roomv1.RoomMedia { + if strings.TrimSpace(media.URL) == "" && strings.TrimSpace(media.ObjectKey) == "" { + return nil + } + return &roomv1.RoomMedia{ + Url: media.URL, ObjectKey: media.ObjectKey, ContentType: media.ContentType, Format: media.Format, + SizeBytes: media.SizeBytes, Width: media.Width, Height: media.Height, Animated: media.Animated, + FrameCount: media.FrameCount, DurationMs: media.DurationMS, PosterUrl: media.PosterURL, + ThumbnailUrl: media.ThumbnailURL, Sha256: media.SHA256, Status: media.Status, + } +} + +func roomMediaEventSnapshot(media state.RoomMedia) *roomeventsv1.RoomMediaSnapshot { + return &roomeventsv1.RoomMediaSnapshot{ + Url: media.URL, ObjectKey: media.ObjectKey, ContentType: media.ContentType, Format: media.Format, + SizeBytes: media.SizeBytes, Width: media.Width, Height: media.Height, Animated: media.Animated, + FrameCount: media.FrameCount, DurationMs: media.DurationMS, PosterUrl: media.PosterURL, + ThumbnailUrl: media.ThumbnailURL, Sha256: media.SHA256, + } +} + +func roomBackgroundState(background RoomBackgroundImage) state.RoomBackground { + return state.RoomBackground{ + BackgroundID: background.BackgroundID, RoomID: background.RoomID, CreatedByUserID: background.CreatedByUserID, + CreatedAtMS: background.CreatedAtMS, UpdatedAtMS: background.UpdatedAtMS, CommandID: background.CommandID, + Media: background.Media, + } +} + +func roomBackgroundToProto(background RoomBackgroundImage) *roomv1.RoomBackgroundImage { + return &roomv1.RoomBackgroundImage{ + BackgroundId: background.BackgroundID, RoomId: background.RoomID, ImageUrl: background.ImageURL, + CreatedByUserId: background.CreatedByUserID, CreatedAtMs: background.CreatedAtMS, UpdatedAtMs: background.UpdatedAtMS, + Media: roomMediaToProto(background.Media), CommandId: background.CommandID, + } +} + +func roomBackgroundsToProto(backgrounds []RoomBackgroundImage) []*roomv1.RoomBackgroundImage { + items := make([]*roomv1.RoomBackgroundImage, 0, len(backgrounds)) + for _, background := range backgrounds { + items = append(items, roomBackgroundToProto(background)) + } + return items +} + +func legacyRoomBackgroundCommandID(rawAppCode string, roomID string, actorUserID int64, imageURL string) string { + // legacy 请求没有 command_id;用租户、房间、房主和 URL 生成稳定短键,重试不会重复建素材, + // 同时不把 URL 明文放进 VARCHAR(128) 或日志索引。 + sum := sha256.Sum256([]byte(strings.Join([]string{ + appcode.Normalize(rawAppCode), strings.TrimSpace(roomID), fmt.Sprintf("%d", actorUserID), strings.TrimSpace(imageURL), + }, "\x00"))) + return "legacy_bg_" + hex.EncodeToString(sum[:16]) +} diff --git a/services/room-service/internal/room/service/online_users_test.go b/services/room-service/internal/room/service/online_users_test.go index f75b94d7..0d78666c 100644 --- a/services/room-service/internal/room/service/online_users_test.go +++ b/services/room-service/internal/room/service/online_users_test.go @@ -138,6 +138,55 @@ func TestJoinLeaveRoomUpdatesPresenceIncrementally(t *testing.T) { } } +func TestBatchGetUserVoiceRoomPresencesUsesCurrentMicSeatAndHidesViewer(t *testing.T) { + ctx := context.Background() + repository := mysqltest.NewRepository(t) + now := &fixedRoomRocketClock{now: time.Date(2026, 7, 17, 8, 0, 0, 0, time.UTC)} + svc := newRocketTestService(t, repository, &rocketTestWallet{}, now) + + roomID := "room-profile-live" + ownerID := int64(1301) + targetID := int64(1302) + missingID := int64(1303) + offMicID := int64(1304) + createRocketRoom(t, ctx, svc, roomID, ownerID, 9001) + joinRocketRoom(t, ctx, svc, roomID, targetID) + joinRocketRoom(t, ctx, svc, roomID, offMicID) + if _, err := svc.MicUp(ctx, &roomv1.MicUpRequest{Meta: rocketMeta(roomID, targetID, "profile-live-mic-up"), SeatNo: 3}); err != nil { + t.Fatalf("mic up target failed: %v", err) + } + if _, err := svc.SetRoomPassword(ctx, &roomv1.SetRoomPasswordRequest{Meta: rocketMeta(roomID, ownerID, "profile-live-lock"), Locked: true, Password: "1234"}); err != nil { + t.Fatalf("lock room failed: %v", err) + } + + resp, err := svc.BatchGetUserVoiceRoomPresences(ctx, &roomv1.BatchGetUserVoiceRoomPresencesRequest{ + Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RequestId: "req-profile-live"}, + ViewerUserId: ownerID, + UserIds: []int64{targetID, missingID, offMicID, ownerID, targetID}, + }) + if err != nil { + t.Fatalf("batch user voice room presence failed: %v", err) + } + if len(resp.GetItems()) != 4 { + t.Fatalf("deduplicated input must return four ordered items: %+v", resp.GetItems()) + } + target := resp.GetItems()[0] + if target.GetUserId() != targetID || target.GetState() != "on_mic" || !target.GetJoinable() || target.GetSeatNo() != 3 { + t.Fatalf("target mic state mismatch: %+v", target) + } + if room := target.GetRoom(); room == nil || room.GetRoomId() != roomID || room.GetOwnerUserId() != ownerID || !room.GetLocked() || room.GetSeatCount() != 10 { + t.Fatalf("target room entry mismatch: %+v", target.GetRoom()) + } + for _, item := range resp.GetItems()[1:] { + if item.GetState() != "not_on_mic" || item.GetJoinable() || item.GetRoom() != nil || item.GetSeatNo() != 0 { + t.Fatalf("missing user and viewer must not expose a room: %+v", item) + } + } + if resp.GetServerTimeMs() != now.now.UnixMilli() || target.GetObservedAtMs() != resp.GetServerTimeMs() { + t.Fatalf("presence timestamps must use the service UTC clock: %+v", resp) + } +} + func TestMarkRoomUserPresenceLeftDoesNotClearAnotherRoom(t *testing.T) { ctx := appcode.WithContext(context.Background(), appcode.Default) repository := mysqltest.NewRepository(t) diff --git a/services/room-service/internal/room/service/pipeline.go b/services/room-service/internal/room/service/pipeline.go index 668c52d2..41babfd2 100644 --- a/services/room-service/internal/room/service/pipeline.go +++ b/services/room-service/internal/room/service/pipeline.go @@ -146,6 +146,7 @@ func (s *Service) mutateRoom(ctx context.Context, cmd command.Command, replayabl RoomUserContributionPeriodStats: result.roomUserContributionPeriodStats, RoomWeeklyContribution: result.roomWeeklyContribution, GiftOperation: giftOperationCommitForResult(cmd, resultPayload, now), + RoomDecorationAssignment: result.roomDecorationAssignment, }) commitCancel() if errors.Is(err, ErrCommandAlreadyCommitted) { @@ -188,7 +189,7 @@ func (s *Service) mutateRoom(ctx context.Context, cmd command.Command, replayabl result.applied = true } else { // no-op 命令仍写入 command log,保证相同 command_id 重试可命中幂等, - // 不同 payload 复用 command_id 会被后续入口拒绝为 CONFLICT。 + // 不同 payload 复用 command_id 会被后续入口拒绝为 IDEMPOTENCY_CONFLICT。 if err := s.ensureLeaseStillOwned(ctx, cmd.RoomID(), lease); err != nil { return nil, err } @@ -206,7 +207,9 @@ func (s *Service) mutateRoom(ctx context.Context, cmd command.Command, replayabl LeaseToken: lease.LeaseToken, CreatedAtMS: now.UnixMilli(), } - err := s.repository.SaveMutation(commitCtx, MutationCommit{Command: commandRecord}) + err := s.repository.SaveMutation(commitCtx, MutationCommit{ + Command: commandRecord, RoomDecorationAssignment: result.roomDecorationAssignment, + }) commitCancel() if err != nil && !s.mutationCommitConfirmed(ctx, commandRecord) { return nil, err @@ -313,7 +316,7 @@ func (s *Service) commandRecordForIdempotency(ctx context.Context, cmd command.C return nil, record, true, err } if !matched { - return nil, record, true, xerr.New(xerr.Conflict, "command_id payload conflict") + return nil, record, true, xerr.New(xerr.IdempotencyConflict, "command_id payload conflict") } return payload, record, true, nil } @@ -327,7 +330,7 @@ func (s *Service) commandRecordForIdempotency(ctx context.Context, cmd command.C } if record.CommandType != cmd.Type() || !bytes.Equal(storedIDPayload, currentIDPayload) { // 同一个 command_id 携带不同业务载荷必须显式冲突,不能返回旧命令结果掩盖客户端错误。 - return nil, record, true, xerr.New(xerr.Conflict, "command_id payload conflict") + return nil, record, true, xerr.New(xerr.IdempotencyConflict, "command_id payload conflict") } return payload, record, true, nil diff --git a/services/room-service/internal/room/service/recovery.go b/services/room-service/internal/room/service/recovery.go index 2911b773..c4e9255c 100644 --- a/services/room-service/internal/room/service/recovery.go +++ b/services/room-service/internal/room/service/recovery.go @@ -160,7 +160,46 @@ func replay(current *state.RoomState, cmd command.Command, committedAtMS int64) if current.RoomExt == nil { current.RoomExt = make(map[string]string) } - current.RoomExt[roomExtBackgroundKey] = typed.BackgroundURL + backgroundURL := typed.BackgroundURL + if typed.Background.BackgroundID > 0 && typed.Background.Media.URL != "" { + // 新命令完整恢复媒体事实;旧命令没有 Background 时继续只恢复兼容 URL。 + background := typed.Background + current.ActiveBackground = &background + backgroundURL = background.Media.URL + } + current.RoomExt[roomExtBackgroundKey] = backgroundURL + current.Version++ + case *command.ApplyRoomDecoration: + decoration := typed.Resource + switch typed.DecorationType { + case roomDecorationTypeBorder: + current.RoomBorder = decoration.Clone() + case roomDecorationTypeColoredName: + current.RoomNameStyle = decoration.Clone() + default: + return fmt.Errorf("apply_room_decoration replay unsupported type: %s", typed.DecorationType) + } + current.Version++ + case *command.ReconcileRoomDecoration: + var currentDecoration **state.RoomDecoration + switch typed.DecorationType { + case roomDecorationTypeBorder: + currentDecoration = ¤t.RoomBorder + case roomDecorationTypeColoredName: + currentDecoration = ¤t.RoomNameStyle + default: + return fmt.Errorf("reconcile_room_decoration replay unsupported type: %s", typed.DecorationType) + } + if typed.Active { + decoration := typed.Resource + *currentDecoration = decoration.Clone() + } else { + *currentDecoration = nil + } + current.Version++ + case *command.SendRoomImage: + // 图片消息不进入房态字段,但它在原始提交中通过版本推进把 command log 与 outbox 原子绑定; + // 恢复必须重现版本谱系,绝不能再次发送腾讯 IM。 current.Version++ case *command.JoinRoom: if current.Status == state.RoomStatusClosed && typed.ActorUserID() == current.OwnerUserID { diff --git a/services/room-service/internal/room/service/repository.go b/services/room-service/internal/room/service/repository.go index de2423e5..dfe23520 100644 --- a/services/room-service/internal/room/service/repository.go +++ b/services/room-service/internal/room/service/repository.go @@ -6,6 +6,7 @@ import ( "errors" "hyapp/services/room-service/internal/room/outbox" + "hyapp/services/room-service/internal/room/state" ) // RoomMeta 对应 rooms 表中房间的持久元数据。 @@ -85,6 +86,8 @@ type MutationCommit struct { RoomWeeklyContribution *RoomWeeklyContributionIncrement // GiftOperation 非 nil 时必须在同一事务内把 saga 推进到 committed,并保存与 command log 完全一致的结果。 GiftOperation *GiftOperationCommit + // RoomDecorationAssignment 与装扮命令同事务更新,供 wallet 状态事件按索引即时定位并纠正 Room Cell。 + RoomDecorationAssignment *RoomDecorationAssignmentChange } var ( @@ -181,15 +184,73 @@ type RoomCloseInfo struct { // RoomBackgroundImage 是房间 owner 保存过的背景图素材。 // 当前生效背景仍属于 Room Cell 快照;该表只保存可选素材列表。 type RoomBackgroundImage struct { - AppCode string - BackgroundID int64 - RoomID string - ImageURL string + AppCode string + BackgroundID int64 + RoomID string + ImageURL string + // CommandID 让“上传后保存素材”具备业务幂等语义;同 ID 不同媒体必须显式冲突。 + CommandID string + // Media 是 gateway 专用上传入口验证后的完整媒体事实,禁止后续按 URL 后缀猜格式。 + Media state.RoomMedia CreatedByUserID int64 CreatedAtMS int64 UpdatedAtMS int64 } +// ErrRoomBackgroundCommandConflict 表示 command_id 已用于另一份背景媒体,service 映射为幂等冲突。 +var ErrRoomBackgroundCommandConflict = errors.New("room background command payload conflict") + +// RoomMediaUploadRegistration 是 COS PutObject 前由 room owner 持久化的上传授权事实。 +// 同一 app/room/command 只能绑定一个 actor、purpose 和规范化媒体 payload,避免换文件重试产生第二个对象。 +type RoomMediaUploadRegistration struct { + AppCode string + RoomID string + CommandID string + ActorUserID int64 + Purpose string + MediaPayloadSHA256 string + ExpectedObjectKey string + ObjectURL string + Status string + Media state.RoomMedia + AuthorizedVersion int64 + CreatedAtMS int64 + UpdatedAtMS int64 +} + +// ErrRoomMediaUploadCommandConflict 表示上传 command_id 已绑定另一份文件或用途。 +var ErrRoomMediaUploadCommandConflict = errors.New("room media upload command payload conflict") + +// RoomDecorationAssignment 是 Room Cell 当前装扮对应的低频反向索引。 +// wallet 事件只能通过该表按 resource_id/owner_user_id 定位房间,禁止扫描 room_list_entries JSON。 +type RoomDecorationAssignment struct { + AppCode string + RoomID string + DecorationType string + OwnerUserID int64 + BenefitCode string + ResourceID int64 + EntitlementID string + ExpiresAtMS int64 + UpdatedAtMS int64 +} + +// RoomDecorationAssignmentChange 与应用/清除 Room Cell 装扮在同一 SaveMutation 事务提交。 +// Assignment=nil 表示删除该 decoration_type 的当前反向索引。 +type RoomDecorationAssignmentChange struct { + DecorationType string + ExpectedResourceID int64 + Assignment *RoomDecorationAssignment +} + +type RoomDecorationAssignmentQuery struct { + OwnerUserID int64 + ResourceID int64 + AfterRoomID string + AfterDecorationType string + Limit int +} + // SnapshotRecord 对应 room_snapshots 中的一条最新快照。 type SnapshotRecord struct { // AppCode 是快照所属 App,恢复时必须和房间 meta 保持一致。 @@ -238,6 +299,9 @@ type RoomListEntry struct { OccupiedSeatCount int32 // SortScore 是热门列表排序分,算法不进入 RoomState。 SortScore int64 + // RoomBorderJSON/RoomNameStyleJSON 是低频装扮快照;读取时必须再次按 expires_at_ms 过滤。 + RoomBorderJSON string + RoomNameStyleJSON string // CreatedAtMS 是创建房间时的毫秒时间,new tab 使用它排序。 CreatedAtMS int64 // UpdatedAtMS 是投影最后更新时间。 @@ -823,6 +887,29 @@ type RoomMetaStore interface { ListRoomBackgrounds(ctx context.Context, roomID string) ([]RoomBackgroundImage, error) } +// RoomBackgroundCommandReader 是新版背景保存的可选强幂等读能力。 +// 它不并入 RoomMetaStore,避免旧测试/内存仓储为纯优化路径扩散接口;生产 MySQL 实现该接口后, +// 已成功 command_id 的重试可以在房间关闭、owner 元数据变化或 VIP 到期后仍返回第一次保存结果。 +type RoomBackgroundCommandReader interface { + GetRoomBackgroundByCommand(ctx context.Context, roomID string, commandID string) (RoomBackgroundImage, bool, error) +} + +// RoomMediaUploadGuardStore 是专用上传入口的强幂等持久边界。 +// Get 必须先于可变房态/VIP 校验,Register 则只在首次授权通过后原子占用 command_id。 +type RoomMediaUploadGuardStore interface { + GetRoomMediaUploadRegistration(ctx context.Context, roomID string, commandID string) (RoomMediaUploadRegistration, bool, error) + GetActiveRoomMediaUploadByObjectKey(ctx context.Context, objectKey string) (RoomMediaUploadRegistration, bool, error) + RegisterRoomMediaUpload(ctx context.Context, registration RoomMediaUploadRegistration) (RoomMediaUploadRegistration, error) + CompleteRoomMediaUpload(ctx context.Context, registration RoomMediaUploadRegistration) (RoomMediaUploadRegistration, error) +} + +// RoomDecorationReconciliationStore 保存装扮反向索引和 wallet consumer 本地幂等事实。 +type RoomDecorationReconciliationStore interface { + ListRoomDecorationAssignments(ctx context.Context, query RoomDecorationAssignmentQuery) ([]RoomDecorationAssignment, error) + HasConsumedWalletDecorationEvent(ctx context.Context, eventID string) (bool, error) + MarkWalletDecorationEventConsumed(ctx context.Context, eventID string, eventType string, consumedAtMS int64) error +} + // CommandStore 保存命令幂等记录和一次 Room Cell mutation 的事务提交边界。 type CommandStore interface { // GetCommand 读取已提交命令,用于幂等重试和 command_id 冲突判定。 @@ -911,6 +998,8 @@ type PresenceStore interface { MarkRoomUserPresenceLeft(ctx context.Context, presence RoomPresence) error // GetCurrentRoomPresence 查询用户当前 active 房间 presence,不扫描房间列表或快照。 GetCurrentRoomPresence(ctx context.Context, userID int64) (RoomPresence, bool, error) + // BatchGetCurrentRoomPresences 用用户主键批量读取候选房间;Room Cell 快照仍是最终上麦事实。 + BatchGetCurrentRoomPresences(ctx context.Context, userIDs []int64) (map[int64]RoomPresence, error) // ListRoomOnlineUsers 查询房间 active 在线用户分页,不读取完整 RoomSnapshot。 ListRoomOnlineUsers(ctx context.Context, query RoomOnlineUserQuery) (RoomOnlineUserPage, error) } diff --git a/services/room-service/internal/room/service/service.go b/services/room-service/internal/room/service/service.go index c337acc1..91a0c027 100644 --- a/services/room-service/internal/room/service/service.go +++ b/services/room-service/internal/room/service/service.go @@ -224,6 +224,8 @@ type mutationResult struct { roomUserGiftStats []RoomUserGiftStatIncrement // roomWeeklyContribution 是后台房间贡献列的当前 UTC 周增量,和命令日志同事务提交。 roomWeeklyContribution *RoomWeeklyContributionIncrement + // roomDecorationAssignment 和 Room Cell 装扮状态同事务更新,供 wallet 事件按索引即时重算。 + roomDecorationAssignment *RoomDecorationAssignmentChange } // New 初始化 room-service 领域服务。 diff --git a/services/room-service/internal/room/service/user_voice_room_presence.go b/services/room-service/internal/room/service/user_voice_room_presence.go new file mode 100644 index 00000000..89893ec4 --- /dev/null +++ b/services/room-service/internal/room/service/user_voice_room_presence.go @@ -0,0 +1,167 @@ +package service + +import ( + "context" + "log/slog" + "strings" + "time" + + roomv1 "hyapp.local/api/proto/room/v1" + "hyapp/pkg/appcode" + "hyapp/pkg/logx" + "hyapp/pkg/xerr" + "hyapp/services/room-service/internal/room/state" +) + +const ( + maxUserVoiceRoomPresenceBatchSize = 100 + userVoiceRoomStateOnMic = "on_mic" + userVoiceRoomStateNotOnMic = "not_on_mic" +) + +// BatchGetUserVoiceRoomPresences 返回个人资料页可以安全展示的实时上麦入口。 +// room_user_presence 只负责把 user_id 定位到候选 Room Cell;上麦、房间生命周期和 viewer 权限最终都以最新快照为准。 +func (s *Service) BatchGetUserVoiceRoomPresences(ctx context.Context, req *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) { + startedAt := time.Now() + if req == nil || req.GetViewerUserId() <= 0 { + return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required") + } + + userIDs, ok := normalizedVoiceRoomPresenceUserIDs(req.GetUserIds()) + if !ok { + return nil, xerr.New(xerr.InvalidArgument, "user_ids must contain 1 to 100 positive ids") + } + ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) + + presences, err := s.repository.BatchGetCurrentRoomPresences(ctx, userIDs) + if err != nil { + return nil, err + } + + // 一个 batch 可能包含同房间多个麦上用户;每个候选房间只恢复/读取一次快照,避免重复触发 Room Cell 或 MySQL 恢复链路。 + snapshots := make(map[string]*roomv1.RoomSnapshot) + for userID, presence := range presences { + if userID == req.GetViewerUserId() || strings.TrimSpace(presence.RoomID) == "" { + continue + } + if _, loaded := snapshots[presence.RoomID]; loaded { + continue + } + snapshot, err := s.currentSnapshot(ctx, presence.RoomID) + if err != nil { + return nil, err + } + snapshots[presence.RoomID] = snapshot + } + + nowMS := s.clock.Now().UnixMilli() + resolved := make(map[int64]*roomv1.UserVoiceRoomPresence, len(userIDs)) + onMicCount := 0 + hiddenCount := 0 + orphanCount := 0 + for _, userID := range userIDs { + item := &roomv1.UserVoiceRoomPresence{ + UserId: userID, + State: userVoiceRoomStateNotOnMic, + ObservedAtMs: nowMS, + } + resolved[userID] = item + + // 当前产品只在客态资料页展示 Live;查询本人时按不可见结果收敛,不泄露多余房间入口。 + if userID == req.GetViewerUserId() { + hiddenCount++ + continue + } + presence, exists := presences[userID] + if !exists { + continue + } + snapshot := snapshots[presence.RoomID] + if snapshot == nil || snapshot.GetStatus() != state.RoomStatusActive || snapshot.GetAppCode() != appcode.FromContext(ctx) { + // presence 投影允许短暂滞后;孤儿索引只能降级为 not_on_mic,不能返回旧 room_id。 + orphanCount++ + continue + } + if snapshotUserBanned(snapshot, req.GetViewerUserId(), nowMS) { + // viewer 被房间封禁时正式进房必然失败,因此这里隐藏全部房间定位字段。 + hiddenCount++ + continue + } + seat := protoSeatByUser(snapshot, userID) + if seat == nil { + // 仅在房间内但没有占用麦位不是上麦状态。 + continue + } + if findProtoUser(snapshot, userID) == nil { + // 麦位用户不在当前 online_users 说明快照内部事实不完整,按异常数据隐藏并留日志计数。 + orphanCount++ + continue + } + + ext := snapshot.GetRoomExt() + roomShortID := snapshot.GetRoomShortId() + if roomShortID == "" { + roomShortID = ext["room_short_id"] + } + onlineCount := snapshot.GetOnlineCount() + if onlineCount <= 0 && len(snapshot.GetOnlineUsers()) > 0 { + onlineCount = int32(len(snapshot.GetOnlineUsers())) + } + item.State = userVoiceRoomStateOnMic + item.SeatNo = seat.GetSeatNo() + item.Joinable = true + item.Room = &roomv1.UserVoiceRoomPresenceRoom{ + RoomId: snapshot.GetRoomId(), + RoomShortId: roomShortID, + OwnerUserId: snapshot.GetOwnerUserId(), + HostUserId: snapshot.GetOwnerUserId(), + Title: ext["title"], + CoverUrl: ext["cover_url"], + BackgroundUrl: ext["background_url"], + Mode: snapshot.GetMode(), + OnlineCount: onlineCount, + SeatCount: int32(len(snapshot.GetMicSeats())), + Locked: snapshot.GetLocked(), + } + onMicCount++ + } + + items := make([]*roomv1.UserVoiceRoomPresence, 0, len(userIDs)) + for _, userID := range userIDs { + items = append(items, resolved[userID]) + } + logx.Info(ctx, "user_voice_room_presence_batch_queried", + slog.String("request_id", req.GetMeta().GetRequestId()), + slog.String("app_code", appcode.FromContext(ctx)), + slog.Int64("viewer_user_id", req.GetViewerUserId()), + slog.Int("user_count", len(userIDs)), + slog.Int("active_presence_count", len(presences)), + slog.Int("on_mic_count", onMicCount), + slog.Int("hidden_count", hiddenCount), + slog.Int("orphan_count", orphanCount), + slog.Int64("duration_ms", time.Since(startedAt).Milliseconds()), + ) + return &roomv1.BatchGetUserVoiceRoomPresencesResponse{Items: items, ServerTimeMs: nowMS}, nil +} + +func normalizedVoiceRoomPresenceUserIDs(values []int64) ([]int64, bool) { + if len(values) == 0 { + return nil, false + } + seen := make(map[int64]struct{}, len(values)) + result := make([]int64, 0, len(values)) + for _, userID := range values { + if userID <= 0 { + return nil, false + } + if _, exists := seen[userID]; exists { + continue + } + seen[userID] = struct{}{} + result = append(result, userID) + if len(result) > maxUserVoiceRoomPresenceBatchSize { + return nil, false + } + } + return result, len(result) > 0 +} diff --git a/services/room-service/internal/room/service/vip.go b/services/room-service/internal/room/service/vip.go index 6cf20563..4746c380 100644 --- a/services/room-service/internal/room/service/vip.go +++ b/services/room-service/internal/room/service/vip.go @@ -10,6 +10,7 @@ import ( walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" + "hyapp/services/room-service/internal/integration" ) const ( @@ -18,6 +19,9 @@ const ( tieredPrivilegeVIPProgram = "tiered_privilege_v1" vipBenefitCustomRoomBackground = "custom_room_background" + vipBenefitRoomImageMessage = "room_image_message" + vipBenefitRoomBorder = "room_border" + vipBenefitColoredRoomName = "colored_room_name" vipBenefitAntiKick = "anti_kick" vipBenefitAntiMute = "anti_mute" vipBenefitRoomEntryNotice = "room_entry_notice" @@ -107,9 +111,47 @@ func (s *Service) loadEffectiveVIPAccess(ctx context.Context, requestID string, } // requireCustomRoomBackgroundBenefit 只对配置为 P1 权益制的 App 执行自定义背景门禁。 +// action 由调用点传入精确动作,Flutter 才能区分上传、保存和切换失败并保留对应本地状态。 // legacy_timed 保持 Lalu 原行为;新 App 只需在 wallet 切换 program_type,无需再修改 room-service。 -func (s *Service) requireCustomRoomBackgroundBenefit(ctx context.Context, requestID string, ownerUserID int64) error { - access, err := s.loadEffectiveVIPAccess(ctx, requestID, ownerUserID) +func (s *Service) requireCustomRoomBackgroundBenefit(ctx context.Context, requestID string, ownerUserID int64, action string) error { + return s.requireVIPBenefit(ctx, requestID, ownerUserID, vipBenefitCustomRoomBackground, action) +} + +// requireVIPBenefit 优先调用 wallet 的单项权益判定,并把升级页需要的等级事实放进错误 metadata。 +// legacy_timed App 沿用旧行为;P1 App 在 wallet 不可用或没有权益时都失败关闭,避免故障窗口越权。 +func (s *Service) requireVIPBenefit(ctx context.Context, requestID string, userID int64, benefitCode string, action string) error { + if checker, ok := s.wallet.(integration.WalletVIPBenefitClient); ok { + lookupCtx, cancel := context.WithTimeout(ctx, roomVIPLookupTimeout) + defer cancel() + response, err := checker.CheckVipBenefit(lookupCtx, &walletv1.CheckVipBenefitRequest{ + RequestId: strings.TrimSpace(requestID), + AppCode: appcode.FromContext(ctx), + UserId: userID, + BenefitCode: normalizeVIPCode(benefitCode), + }) + if err != nil { + // 对外错误只暴露稳定的业务语义,底层 gRPC 状态和地址不能进入 HTTP envelope。 + return xerr.New(xerr.Unavailable, "check vip benefit failed") + } + if response == nil || response.GetState() == nil || response.GetState().GetProgramConfig() == nil { + return xerr.New(xerr.Unavailable, "wallet vip benefit state is incomplete") + } + if normalizeVIPCode(response.GetState().GetProgramConfig().GetProgramType()) != tieredPrivilegeVIPProgram { + return nil + } + if response.GetAllowed() { + return nil + } + return xerr.NewWithMetadata(xerr.VIPBenefitRequired, "vip benefit is required", map[string]string{ + "benefit_code": normalizeVIPCode(benefitCode), + "required_level": fmt.Sprintf("%d", response.GetRequiredLevel()), + "current_effective_level": fmt.Sprintf("%d", response.GetCurrentEffectiveLevel()), + "action": strings.TrimSpace(action), + }) + } + + // 兼容滚动升级和现有测试 fake:旧 client 仍可从 effective_benefits 得到同一授权事实。 + access, err := s.loadEffectiveVIPAccess(ctx, requestID, userID) if err != nil { // 权限事实不可用时必须失败关闭;放行会让无权益用户在 wallet 故障窗口获得可持久化背景权限。 return err @@ -117,8 +159,13 @@ func (s *Service) requireCustomRoomBackgroundBenefit(ctx context.Context, reques if access.programType != tieredPrivilegeVIPProgram { return nil } - if !access.hasBenefit(vipBenefitCustomRoomBackground) { - return xerr.New(xerr.VIPBenefitRequired, "custom room background vip benefit is required") + if !access.hasBenefit(benefitCode) { + return xerr.NewWithMetadata(xerr.VIPBenefitRequired, "vip benefit is required", map[string]string{ + "benefit_code": normalizeVIPCode(benefitCode), + "required_level": "0", + "current_effective_level": fmt.Sprintf("%d", access.level), + "action": strings.TrimSpace(action), + }) } return nil } diff --git a/services/room-service/internal/room/service/wallet_decoration_reconcile.go b/services/room-service/internal/room/service/wallet_decoration_reconcile.go new file mode 100644 index 00000000..23d1a2a9 --- /dev/null +++ b/services/room-service/internal/room/service/wallet_decoration_reconcile.go @@ -0,0 +1,296 @@ +package service + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "reflect" + "sort" + "strings" + "time" + + roomeventsv1 "hyapp.local/api/proto/events/room/v1" + "hyapp/pkg/appcode" + "hyapp/pkg/walletmq" + "hyapp/pkg/xerr" + "hyapp/services/room-service/internal/room/command" + "hyapp/services/room-service/internal/room/outbox" + "hyapp/services/room-service/internal/room/rank" + "hyapp/services/room-service/internal/room/state" +) + +const ( + walletEventResourceChanged = "ResourceChanged" + walletEventResourceGrantRevoked = "ResourceGrantRevoked" + walletEventUserResourceChanged = "UserResourceChanged" + walletEventVipEffectiveChanged = "VipEffectiveChanged" + walletEventVipProgramChanged = "VipProgramChanged" + roomDecorationReconcileBatchSize = 100 +) + +// HandleWalletDecorationEvent 消费 wallet owner 已提交的权限/资源变化,并通过 Room Cell 命令即时更新装扮。 +// 专用 assignment 表提供反向索引;每个房间的 command log 和全局 consumption 表共同承担 at-least-once 幂等。 +func (s *Service) HandleWalletDecorationEvent(ctx context.Context, message walletmq.WalletOutboxMessage) error { + queries, handled, err := walletDecorationAssignmentQueries(message) + if err != nil || !handled { + return err + } + store, ok := s.repository.(RoomDecorationReconciliationStore) + if !ok { + return xerr.New(xerr.Unavailable, "room decoration reconciliation store is unavailable") + } + ctx = appcode.WithContext(ctx, message.AppCode) + consumed, err := store.HasConsumedWalletDecorationEvent(ctx, message.EventID) + if err != nil || consumed { + return err + } + + assignments, err := listWalletEventDecorationAssignments(ctx, store, queries) + if err != nil { + return err + } + for _, assignment := range assignments { + if err := s.reconcileRoomDecorationAssignment(ctx, message, assignment); err != nil { + return err + } + } + return store.MarkWalletDecorationEventConsumed(ctx, message.EventID, message.EventType, s.clock.Now().UnixMilli()) +} + +func walletDecorationAssignmentQueries(message walletmq.WalletOutboxMessage) ([]RoomDecorationAssignmentQuery, bool, error) { + switch strings.TrimSpace(message.EventType) { + case walletEventResourceChanged: + resourceID := walletPayloadInt64(message.PayloadJSON, "resource_id", "resourceid") + if resourceID <= 0 { + return nil, true, fmt.Errorf("ResourceChanged payload has no resource_id") + } + return []RoomDecorationAssignmentQuery{{ResourceID: resourceID}}, true, nil + case walletEventResourceGrantRevoked, walletEventUserResourceChanged, walletEventVipEffectiveChanged: + if message.UserID <= 0 { + return nil, true, fmt.Errorf("%s has no user_id", message.EventType) + } + return []RoomDecorationAssignmentQuery{{OwnerUserID: message.UserID}}, true, nil + case walletEventVipProgramChanged: + resourceIDs := walletPayloadInt64Slice(message.PayloadJSON, + "resource_ids", "old_resource_ids", "new_resource_ids", "oldresourceids", "newresourceids") + if len(resourceIDs) == 0 { + // program status/level_count 变化可能影响所有当前装扮;这里只分页扫描专用 assignment 小表。 + return []RoomDecorationAssignmentQuery{{}}, true, nil + } + queries := make([]RoomDecorationAssignmentQuery, 0, len(resourceIDs)) + for _, resourceID := range resourceIDs { + queries = append(queries, RoomDecorationAssignmentQuery{ResourceID: resourceID}) + } + return queries, true, nil + default: + // legacy Tag 兼容订阅会收到同 topic 的其它钱包事实,非装扮事件必须快速确认且不能写消费表。 + return nil, false, nil + } +} + +func listWalletEventDecorationAssignments(ctx context.Context, store RoomDecorationReconciliationStore, queries []RoomDecorationAssignmentQuery) ([]RoomDecorationAssignment, error) { + byKey := make(map[string]RoomDecorationAssignment) + for _, query := range queries { + query.Limit = roomDecorationReconcileBatchSize + for { + page, err := store.ListRoomDecorationAssignments(ctx, query) + if err != nil { + return nil, err + } + for _, assignment := range page { + byKey[assignment.RoomID+"\x00"+assignment.DecorationType] = assignment + } + if len(page) < query.Limit { + break + } + last := page[len(page)-1] + query.AfterRoomID, query.AfterDecorationType = last.RoomID, last.DecorationType + } + } + assignments := make([]RoomDecorationAssignment, 0, len(byKey)) + for _, assignment := range byKey { + assignments = append(assignments, assignment) + } + sort.Slice(assignments, func(i, j int) bool { + if assignments[i].RoomID != assignments[j].RoomID { + return assignments[i].RoomID < assignments[j].RoomID + } + return assignments[i].DecorationType < assignments[j].DecorationType + }) + return assignments, nil +} + +func (s *Service) reconcileRoomDecorationAssignment(ctx context.Context, message walletmq.WalletOutboxMessage, assignment RoomDecorationAssignment) error { + resourceType, fallbackBenefitCode, err := roomDecorationContract(assignment.DecorationType) + if err != nil { + return err + } + benefitCode := strings.TrimSpace(assignment.BenefitCode) + if benefitCode == "" { + benefitCode = fallbackBenefitCode + } + resource, resolveErr := s.resolveRoomDecoration( + ctx, message.EventID, assignment.OwnerUserID, assignment.ResourceID, assignment.EntitlementID, resourceType, benefitCode, + ) + active := resolveErr == nil + if resolveErr != nil && !walletDecorationInvalidationError(resolveErr) { + // wallet 暂不可用不能被误判成撤销;返回错误让 RocketMQ 保持重试。 + return resolveErr + } + + cmd := command.ReconcileRoomDecoration{ + Base: command.Base{ + AppCode: appcode.FromContext(ctx), RequestID: message.EventID, + CommandID: walletDecorationCommandID(message.EventID, assignment.RoomID, assignment.DecorationType), + ActorID: assignment.OwnerUserID, Room: assignment.RoomID, GatewayNodeID: "wallet_outbox", + SentAtMS: message.OccurredAtMS, + }, + DecorationType: assignment.DecorationType, ExpectedResourceID: assignment.ResourceID, + BenefitCode: benefitCode, SourceEventID: message.EventID, Active: active, Resource: resource, + } + _, err = s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { + var currentDecoration **state.RoomDecoration + switch cmd.DecorationType { + case roomDecorationTypeBorder: + currentDecoration = ¤t.RoomBorder + case roomDecorationTypeColoredName: + currentDecoration = ¤t.RoomNameStyle + default: + return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "decoration_type is invalid") + } + if *currentDecoration == nil || (*currentDecoration).ResourceID != cmd.ExpectedResourceID { + // assignment 查询后房主可能已切换资源;条件删除旧索引,不触碰当前新装扮。 + return mutationResult{ + snapshot: current.ToProtoAt(now.UnixMilli()), + roomDecorationAssignment: &RoomDecorationAssignmentChange{ + DecorationType: cmd.DecorationType, ExpectedResourceID: cmd.ExpectedResourceID, + }, + }, nil, nil + } + if cmd.Active && reflect.DeepEqual(**currentDecoration, cmd.Resource) { + return mutationResult{ + snapshot: current.ToProtoAt(now.UnixMilli()), + roomDecorationAssignment: roomDecorationAssignmentChange(current, cmd.DecorationType, cmd.BenefitCode, cmd.Resource, now.UnixMilli()), + }, nil, nil + } + + if cmd.Active { + decoration := cmd.Resource + *currentDecoration = decoration.Clone() + } else { + *currentDecoration = nil + } + current.Version++ + changed := cmd.Resource + if !cmd.Active { + changed = state.RoomDecoration{} + } + event, buildErr := outbox.Build(current.RoomID, "RoomDecorationChanged", current.Version, now, &roomeventsv1.RoomDecorationChanged{ + ActorUserId: cmd.ActorUserID(), DecorationType: cmd.DecorationType, ResourceId: changed.ResourceID, + ResourceCode: changed.ResourceCode, ResourceType: changed.ResourceType, Name: changed.Name, + AssetUrl: changed.AssetURL, PreviewUrl: changed.PreviewURL, AnimationUrl: changed.AnimationURL, + Format: changed.Format, MetadataJson: changed.MetadataJSON, TextColors: append([]string(nil), changed.TextColors...), + Stops: append([]float64(nil), changed.Stops...), AngleDegrees: changed.AngleDegrees, + EntitlementId: changed.EntitlementID, ExpiresAtMs: changed.ExpiresAtMS, + }) + if buildErr != nil { + return mutationResult{}, nil, buildErr + } + assignmentChange := &RoomDecorationAssignmentChange{ + DecorationType: cmd.DecorationType, ExpectedResourceID: cmd.ExpectedResourceID, + } + if cmd.Active { + assignmentChange = roomDecorationAssignmentChange(current, cmd.DecorationType, cmd.BenefitCode, cmd.Resource, now.UnixMilli()) + } + return mutationResult{ + snapshot: current.ToProtoAt(now.UnixMilli()), roomDecorationAssignment: assignmentChange, + }, []outbox.Record{event}, nil + }) + if xerr.IsCode(err, xerr.NotFound) { + // 房间与 assignment 删除并发时,删除事务已经完成 owner 清理,该 wallet 事件可安全确认。 + return nil + } + return err +} + +func roomDecorationContract(decorationType string) (string, string, error) { + switch strings.TrimSpace(decorationType) { + case roomDecorationTypeBorder: + return roomDecorationResourceBorder, vipBenefitRoomBorder, nil + case roomDecorationTypeColoredName: + return roomDecorationResourceName, vipBenefitColoredRoomName, nil + default: + return "", "", xerr.New(xerr.InvalidArgument, "decoration_type is invalid") + } +} + +func walletDecorationInvalidationError(err error) bool { + switch xerr.CodeOf(err) { + case xerr.VIPBenefitRequired, xerr.NotFound, xerr.InvalidArgument: + return true + default: + return false + } +} + +func walletDecorationCommandID(eventID string, roomID string, decorationType string) string { + sum := sha256.Sum256([]byte(strings.Join([]string{eventID, roomID, decorationType}, "\x00"))) + return "wallet_deco_" + hex.EncodeToString(sum[:16]) +} + +func walletPayloadInt64(payload string, keys ...string) int64 { + values := walletPayloadObject(payload) + for _, key := range keys { + if raw, exists := values[normalizeWalletPayloadKey(key)]; exists { + var value int64 + if json.Unmarshal(raw, &value) == nil && value > 0 { + return value + } + } + } + return 0 +} + +func walletPayloadInt64Slice(payload string, keys ...string) []int64 { + values := walletPayloadObject(payload) + seen := make(map[int64]struct{}) + for _, key := range keys { + raw, exists := values[normalizeWalletPayloadKey(key)] + if !exists { + continue + } + var items []int64 + if json.Unmarshal(raw, &items) != nil { + continue + } + for _, item := range items { + if item > 0 { + seen[item] = struct{}{} + } + } + } + out := make([]int64, 0, len(seen)) + for item := range seen { + out = append(out, item) + } + sort.Slice(out, func(i, j int) bool { return out[i] < out[j] }) + return out +} + +func walletPayloadObject(payload string) map[string]json.RawMessage { + var decoded map[string]json.RawMessage + if json.Unmarshal([]byte(payload), &decoded) != nil { + return nil + } + out := make(map[string]json.RawMessage, len(decoded)) + for key, value := range decoded { + out[normalizeWalletPayloadKey(key)] = value + } + return out +} + +func normalizeWalletPayloadKey(value string) string { + return strings.ReplaceAll(strings.ToLower(strings.TrimSpace(value)), "_", "") +} diff --git a/services/room-service/internal/room/state/state.go b/services/room-service/internal/room/state/state.go index e055d962..089e37bd 100644 --- a/services/room-service/internal/room/state/state.go +++ b/services/room-service/internal/room/state/state.go @@ -4,6 +4,7 @@ package state import ( "maps" "slices" + "time" roomv1 "hyapp.local/api/proto/room/v1" ) @@ -116,6 +117,67 @@ type UserModerationState struct { ExpiresAtMS int64 } +// RoomMedia 保存已经通过专用上传入口验证的媒体属性。 +// 这些字段随 Room Cell 快照和 command log 恢复,客户端不需要也不允许按 URL 后缀重新猜类型。 +type RoomMedia struct { + URL string `json:"url"` + ObjectKey string `json:"object_key"` + ContentType string `json:"content_type"` + Format string `json:"format"` + SizeBytes int64 `json:"size_bytes"` + Width int32 `json:"width"` + Height int32 `json:"height"` + Animated bool `json:"animated"` + FrameCount int32 `json:"frame_count"` + DurationMS int64 `json:"duration_ms"` + PosterURL string `json:"poster_url,omitempty"` + ThumbnailURL string `json:"thumbnail_url,omitempty"` + SHA256 string `json:"sha256"` + Status string `json:"status"` +} + +// RoomBackground 是当前生效背景的完整素材快照。 +// 背景列表表仍是低频素材目录;一旦应用,恢复只依赖该快照而不反查可能被运营修改的素材行。 +type RoomBackground struct { + BackgroundID int64 `json:"background_id"` + RoomID string `json:"room_id"` + CreatedByUserID int64 `json:"created_by_user_id"` + CreatedAtMS int64 `json:"created_at_ms"` + UpdatedAtMS int64 `json:"updated_at_ms"` + CommandID string `json:"command_id"` + Media RoomMedia `json:"media"` +} + +// RoomDecoration 是房主 entitlement 在应用瞬间固化的资源展示快照。 +// 权限资格仍由 wallet 实时判断;Room Cell 只持有房间当前选择,不复制 entitlement 生命周期算法。 +type RoomDecoration struct { + ResourceID int64 `json:"resource_id"` + ResourceCode string `json:"resource_code"` + ResourceType string `json:"resource_type"` + Name string `json:"name"` + AssetURL string `json:"asset_url"` + PreviewURL string `json:"preview_url"` + AnimationURL string `json:"animation_url"` + Format string `json:"format"` + MetadataJSON string `json:"metadata_json"` + EntitlementID string `json:"entitlement_id"` + TextColors []string `json:"text_colors,omitempty"` + Stops []float64 `json:"stops,omitempty"` + AngleDegrees float64 `json:"angle_degrees,omitempty"` + ExpiresAtMS int64 `json:"expires_at_ms,omitempty"` +} + +// Clone 深拷贝装扮内的颜色和渐变停靠点,避免失败命令通过共享切片污染当前房态。 +func (d *RoomDecoration) Clone() *RoomDecoration { + if d == nil { + return nil + } + cloned := *d + cloned.TextColors = append([]string(nil), d.TextColors...) + cloned.Stops = append([]float64(nil), d.Stops...) + return &cloned +} + // ActiveAt 判断治理状态在指定 UTC 毫秒是否仍然生效。 func (m UserModerationState) ActiveAt(nowMS int64) bool { return m.UserID > 0 && (m.ExpiresAtMS == 0 || m.ExpiresAtMS > nowMS) @@ -257,6 +319,11 @@ type RoomState struct { GiftRank []RankItem // RoomExt 预留少量房间扩展字段,避免 v1 频繁改结构。 RoomExt map[string]string + // ActiveBackground 是当前生效背景的可信媒体快照;RoomExt.background_url 仅兼容旧客户端。 + ActiveBackground *RoomBackground + // RoomBorder/RoomNameStyle 是房间级装扮最终值,必须随 Room Cell 串行提交和恢复。 + RoomBorder *RoomDecoration + RoomNameStyle *RoomDecoration // Rocket 是语音房火箭状态,送礼燃料和发射必须跟随 Room Cell 串行提交。 Rocket *RocketState // Version 是房间状态版本,成功变更后递增,快照和 command log 都依赖它。 @@ -311,6 +378,9 @@ func (s *RoomState) Clone() *RoomState { Heat: s.Heat, GiftRank: append([]RankItem(nil), s.GiftRank...), RoomExt: make(map[string]string, len(s.RoomExt)), + ActiveBackground: cloneRoomBackground(s.ActiveBackground), + RoomBorder: s.RoomBorder.Clone(), + RoomNameStyle: s.RoomNameStyle.Clone(), Rocket: cloneRocketState(s.Rocket), Version: s.Version, } @@ -379,6 +449,12 @@ func (s *RoomState) SeatIndex(seatNo int32) int { // ToProto 把当前内部房间态投影成对外使用的 protobuf 快照。 func (s *RoomState) ToProto() *roomv1.RoomSnapshot { + return s.ToProtoAt(time.Now().UTC().UnixMilli()) +} + +// ToProtoAt 以响应的 server_time_ms 过滤已到期装扮。过期资源仍留在 Room Cell 和 command log +// 供审计/恢复,但绝不能继续出现在 room detail、snapshot 或 feed 的客户端投影中。 +func (s *RoomState) ToProtoAt(nowMS int64) *roomv1.RoomSnapshot { if s == nil { // nil 状态没有可投影快照,调用方按房间不存在处理。 return nil @@ -446,26 +522,29 @@ func (s *RoomState) ToProto() *roomv1.RoomSnapshot { } return &roomv1.RoomSnapshot{ - RoomId: s.RoomID, - OwnerUserId: s.OwnerUserID, - Mode: s.Mode, - VisibleRegionId: s.VisibleRegionID, - Status: s.Status, - ChatEnabled: s.ChatEnabled, - MicSeats: seats, - OnlineUsers: users, - AdminUserIds: sortedSetIDsExcept(s.AdminUsers, s.OwnerUserID), - BanUserIds: sortedModerationIDs(s.BanUsers), - MuteUserIds: sortedSetIDs(s.MuteUsers), - GiftRank: rankItems, - RoomExt: cloneStringMap(s.RoomExt), - Heat: s.Heat, - Version: s.Version, - RoomShortId: s.RoomExt["room_short_id"], - Locked: s.RoomPasswordHash != "", - Rocket: rocketStateToProto(s.Rocket), - BanStates: sortedModerationStates(s.BanUsers), - OnlineCount: int32(len(users)), + RoomId: s.RoomID, + OwnerUserId: s.OwnerUserID, + Mode: s.Mode, + VisibleRegionId: s.VisibleRegionID, + Status: s.Status, + ChatEnabled: s.ChatEnabled, + MicSeats: seats, + OnlineUsers: users, + AdminUserIds: sortedSetIDsExcept(s.AdminUsers, s.OwnerUserID), + BanUserIds: sortedModerationIDs(s.BanUsers), + MuteUserIds: sortedSetIDs(s.MuteUsers), + GiftRank: rankItems, + RoomExt: cloneStringMap(s.RoomExt), + Heat: s.Heat, + Version: s.Version, + RoomShortId: s.RoomExt["room_short_id"], + Locked: s.RoomPasswordHash != "", + Rocket: rocketStateToProto(s.Rocket), + BanStates: sortedModerationStates(s.BanUsers), + OnlineCount: int32(len(users)), + RoomBorder: roomDecorationToProtoAt(s.RoomBorder, nowMS), + RoomNameStyle: roomDecorationToProtoAt(s.RoomNameStyle, nowMS), + ActiveBackground: roomBackgroundToProto(s.ActiveBackground), } } @@ -478,22 +557,25 @@ func FromProto(snapshot *roomv1.RoomSnapshot) *RoomState { // 所有集合重新分配,恢复后的状态可以直接交给 Room Cell 独占使用。 restored := &RoomState{ - RoomID: snapshot.GetRoomId(), - OwnerUserID: snapshot.GetOwnerUserId(), - Mode: snapshot.GetMode(), - VisibleRegionID: snapshot.GetVisibleRegionId(), - Status: snapshot.GetStatus(), - ChatEnabled: snapshot.GetChatEnabled(), - MicSeats: make([]MicSeat, 0, len(snapshot.GetMicSeats())), - OnlineUsers: make(map[int64]*RoomUserState, len(snapshot.GetOnlineUsers())), - AdminUsers: make(map[int64]bool, len(snapshot.GetAdminUserIds())), - BanUsers: make(map[int64]UserModerationState, len(snapshot.GetBanStates())+len(snapshot.GetBanUserIds())), - MuteUsers: make(map[int64]bool, len(snapshot.GetMuteUserIds())), - GiftRank: make([]RankItem, 0, len(snapshot.GetGiftRank())), - RoomExt: cloneStringMap(snapshot.GetRoomExt()), - Rocket: rocketStateFromProto(snapshot.GetRocket()), - Heat: snapshot.GetHeat(), - Version: snapshot.GetVersion(), + RoomID: snapshot.GetRoomId(), + OwnerUserID: snapshot.GetOwnerUserId(), + Mode: snapshot.GetMode(), + VisibleRegionID: snapshot.GetVisibleRegionId(), + Status: snapshot.GetStatus(), + ChatEnabled: snapshot.GetChatEnabled(), + MicSeats: make([]MicSeat, 0, len(snapshot.GetMicSeats())), + OnlineUsers: make(map[int64]*RoomUserState, len(snapshot.GetOnlineUsers())), + AdminUsers: make(map[int64]bool, len(snapshot.GetAdminUserIds())), + BanUsers: make(map[int64]UserModerationState, len(snapshot.GetBanStates())+len(snapshot.GetBanUserIds())), + MuteUsers: make(map[int64]bool, len(snapshot.GetMuteUserIds())), + GiftRank: make([]RankItem, 0, len(snapshot.GetGiftRank())), + RoomExt: cloneStringMap(snapshot.GetRoomExt()), + ActiveBackground: roomBackgroundFromProto(snapshot.GetActiveBackground()), + RoomBorder: roomDecorationFromProto(snapshot.GetRoomBorder()), + RoomNameStyle: roomDecorationFromProto(snapshot.GetRoomNameStyle()), + Rocket: rocketStateFromProto(snapshot.GetRocket()), + Heat: snapshot.GetHeat(), + Version: snapshot.GetVersion(), } for _, seat := range snapshot.GetMicSeats() { @@ -570,6 +652,142 @@ func FromProto(snapshot *roomv1.RoomSnapshot) *RoomState { return restored } +func cloneRoomBackground(input *RoomBackground) *RoomBackground { + if input == nil { + return nil + } + cloned := *input + return &cloned +} + +func roomMediaToProto(media RoomMedia) *roomv1.RoomMedia { + return &roomv1.RoomMedia{ + Url: media.URL, + ObjectKey: media.ObjectKey, + ContentType: media.ContentType, + Format: media.Format, + SizeBytes: media.SizeBytes, + Width: media.Width, + Height: media.Height, + Animated: media.Animated, + FrameCount: media.FrameCount, + DurationMs: media.DurationMS, + PosterUrl: media.PosterURL, + ThumbnailUrl: media.ThumbnailURL, + Sha256: media.SHA256, + Status: media.Status, + } +} + +func roomMediaFromProto(media *roomv1.RoomMedia) RoomMedia { + if media == nil { + return RoomMedia{} + } + return RoomMedia{ + URL: media.GetUrl(), + ObjectKey: media.GetObjectKey(), + ContentType: media.GetContentType(), + Format: media.GetFormat(), + SizeBytes: media.GetSizeBytes(), + Width: media.GetWidth(), + Height: media.GetHeight(), + Animated: media.GetAnimated(), + FrameCount: media.GetFrameCount(), + DurationMS: media.GetDurationMs(), + PosterURL: media.GetPosterUrl(), + ThumbnailURL: media.GetThumbnailUrl(), + SHA256: media.GetSha256(), + Status: media.GetStatus(), + } +} + +func roomBackgroundToProto(background *RoomBackground) *roomv1.RoomBackgroundImage { + if background == nil || background.BackgroundID <= 0 { + return nil + } + return &roomv1.RoomBackgroundImage{ + BackgroundId: background.BackgroundID, + RoomId: background.RoomID, + ImageUrl: background.Media.URL, + CreatedByUserId: background.CreatedByUserID, + CreatedAtMs: background.CreatedAtMS, + UpdatedAtMs: background.UpdatedAtMS, + Media: roomMediaToProto(background.Media), + CommandId: background.CommandID, + } +} + +func roomBackgroundFromProto(background *roomv1.RoomBackgroundImage) *RoomBackground { + if background == nil || background.GetBackgroundId() <= 0 { + return nil + } + media := roomMediaFromProto(background.GetMedia()) + if media.URL == "" { + // 旧快照只有 image_url;恢复时保留兼容 URL,但不会把缺少完整媒体事实的旧素材用于新的保存入口。 + media.URL = background.GetImageUrl() + } + return &RoomBackground{ + BackgroundID: background.GetBackgroundId(), + RoomID: background.GetRoomId(), + CreatedByUserID: background.GetCreatedByUserId(), + CreatedAtMS: background.GetCreatedAtMs(), + UpdatedAtMS: background.GetUpdatedAtMs(), + CommandID: background.GetCommandId(), + Media: media, + } +} + +func roomDecorationToProto(decoration *RoomDecoration) *roomv1.RoomDecorationResource { + return roomDecorationToProtoAt(decoration, time.Now().UTC().UnixMilli()) +} + +func roomDecorationToProtoAt(decoration *RoomDecoration, nowMS int64) *roomv1.RoomDecorationResource { + if decoration == nil || decoration.ResourceID <= 0 { + return nil + } + if decoration.ExpiresAtMS > 0 && nowMS >= decoration.ExpiresAtMS { + return nil + } + return &roomv1.RoomDecorationResource{ + ResourceId: decoration.ResourceID, + ResourceCode: decoration.ResourceCode, + ResourceType: decoration.ResourceType, + Name: decoration.Name, + AssetUrl: decoration.AssetURL, + PreviewUrl: decoration.PreviewURL, + AnimationUrl: decoration.AnimationURL, + Format: decoration.Format, + MetadataJson: decoration.MetadataJSON, + EntitlementId: decoration.EntitlementID, + TextColors: append([]string(nil), decoration.TextColors...), + Stops: append([]float64(nil), decoration.Stops...), + AngleDegrees: decoration.AngleDegrees, + ExpiresAtMs: decoration.ExpiresAtMS, + } +} + +func roomDecorationFromProto(decoration *roomv1.RoomDecorationResource) *RoomDecoration { + if decoration == nil || decoration.GetResourceId() <= 0 { + return nil + } + return &RoomDecoration{ + ResourceID: decoration.GetResourceId(), + ResourceCode: decoration.GetResourceCode(), + ResourceType: decoration.GetResourceType(), + Name: decoration.GetName(), + AssetURL: decoration.GetAssetUrl(), + PreviewURL: decoration.GetPreviewUrl(), + AnimationURL: decoration.GetAnimationUrl(), + Format: decoration.GetFormat(), + MetadataJSON: decoration.GetMetadataJson(), + EntitlementID: decoration.GetEntitlementId(), + TextColors: append([]string(nil), decoration.GetTextColors()...), + Stops: append([]float64(nil), decoration.GetStops()...), + AngleDegrees: decoration.GetAngleDegrees(), + ExpiresAtMS: decoration.GetExpiresAtMs(), + } +} + func sortedSetIDs(values map[int64]bool) []int64 { // map 转数组时只导出 value=true 的成员,false 等价于不存在。 ids := make([]int64, 0, len(values)) diff --git a/services/room-service/internal/storage/mysql/command_log.go b/services/room-service/internal/storage/mysql/command_log.go index cdd26875..46bf49b4 100644 --- a/services/room-service/internal/storage/mysql/command_log.go +++ b/services/room-service/internal/storage/mysql/command_log.go @@ -133,6 +133,45 @@ func (r *Repository) SaveMutation(ctx context.Context, commit roomservice.Mutati return err } + if change := commit.RoomDecorationAssignment; change != nil { + decorationType := strings.TrimSpace(change.DecorationType) + if decorationType == "" { + _ = tx.Rollback() + return errors.New("room decoration assignment type is required") + } + if change.Assignment == nil { + statement := `DELETE FROM room_decoration_assignments WHERE app_code = ? AND room_id = ? AND decoration_type = ?` + args := []any{appCode, commit.Command.RoomID, decorationType} + if change.ExpectedResourceID > 0 { + // wallet 事件可能和房主切换新资源并发;只允许旧事件删除自己查询到的 assignment。 + statement += ` AND resource_id = ?` + args = append(args, change.ExpectedResourceID) + } + if _, err := tx.ExecContext(ctx, statement, args...); err != nil { + _ = tx.Rollback() + return err + } + } else { + assignment := change.Assignment + if _, err := tx.ExecContext(ctx, ` + INSERT INTO room_decoration_assignments ( + app_code, room_id, decoration_type, owner_user_id, benefit_code, + resource_id, entitlement_id, expires_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + owner_user_id = VALUES(owner_user_id), benefit_code = VALUES(benefit_code), + resource_id = VALUES(resource_id), entitlement_id = VALUES(entitlement_id), + expires_at_ms = VALUES(expires_at_ms), updated_at_ms = VALUES(updated_at_ms)`, + appCode, commit.Command.RoomID, decorationType, assignment.OwnerUserID, + strings.TrimSpace(assignment.BenefitCode), assignment.ResourceID, + strings.TrimSpace(assignment.EntitlementID), assignment.ExpiresAtMS, assignment.UpdatedAtMS, + ); err != nil { + _ = tx.Rollback() + return err + } + } + } + if commit.DeleteRoom { if err := r.deleteRoomRows(ctx, tx, appCode, commit.Command.RoomID); err != nil { _ = tx.Rollback() @@ -429,6 +468,8 @@ func (r *Repository) deleteRoomRows(ctx context.Context, tx *sql.Tx, appCode str `DELETE FROM room_user_gift_stats WHERE app_code = ? AND room_id = ?`, `DELETE FROM room_user_contribution_period_stats WHERE app_code = ? AND room_id = ?`, `DELETE FROM room_background_images WHERE app_code = ? AND room_id = ?`, + `DELETE FROM room_media_upload_registrations WHERE app_code = ? AND room_id = ?`, + `DELETE FROM room_decoration_assignments WHERE app_code = ? AND room_id = ?`, `DELETE FROM room_snapshots WHERE app_code = ? AND room_id = ?`, `DELETE FROM room_gift_operations WHERE app_code = ? AND room_id = ?`, `DELETE FROM room_command_log WHERE app_code = ? AND room_id = ?`, diff --git a/services/room-service/internal/storage/mysql/presence.go b/services/room-service/internal/storage/mysql/presence.go index 3c55c553..e6253e19 100644 --- a/services/room-service/internal/storage/mysql/presence.go +++ b/services/room-service/internal/storage/mysql/presence.go @@ -168,6 +168,57 @@ func (r *Repository) GetCurrentRoomPresence(ctx context.Context, userID int64) ( return presence, true, nil } +// BatchGetCurrentRoomPresences 按 (app_code, user_id) 主键批量读取用户当前房间索引。 +// 单批最多由 service 层传入 100 个 ID;IN 条件仍命中主键前缀,不会扫描 active presence 全表。 +func (r *Repository) BatchGetCurrentRoomPresences(ctx context.Context, userIDs []int64) (map[int64]roomservice.RoomPresence, error) { + if len(userIDs) == 0 { + return map[int64]roomservice.RoomPresence{}, nil + } + + placeholders := strings.TrimSuffix(strings.Repeat("?,", len(userIDs)), ",") + args := make([]any, 0, len(userIDs)+2) + args = append(args, appcode.FromContext(ctx), roomPresenceStatusActiveSQL) + for _, userID := range userIDs { + args = append(args, userID) + } + + rows, err := r.db.QueryContext(ctx, + `SELECT app_code, user_id, room_id, role, publish_state, mic_session_id, room_version, status, joined_at_ms, last_seen_at_ms, updated_at_ms + FROM room_user_presence + WHERE app_code = ? AND status = ? AND user_id IN (`+placeholders+`)`, + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + result := make(map[int64]roomservice.RoomPresence, len(userIDs)) + for rows.Next() { + var presence roomservice.RoomPresence + if err := rows.Scan( + &presence.AppCode, + &presence.UserID, + &presence.RoomID, + &presence.Role, + &presence.PublishState, + &presence.MicSessionID, + &presence.RoomVersion, + &presence.Status, + &presence.JoinedAtMS, + &presence.LastSeenAtMS, + &presence.UpdatedAtMS, + ); err != nil { + return nil, err + } + result[presence.UserID] = presence + } + if err := rows.Err(); err != nil { + return nil, err + } + return result, nil +} + // ListRoomOnlineUsers 分页查询某个房间当前 active presence。 func (r *Repository) ListRoomOnlineUsers(ctx context.Context, query roomservice.RoomOnlineUserQuery) (roomservice.RoomOnlineUserPage, error) { page := query.Page diff --git a/services/room-service/internal/storage/mysql/room_decoration_assignment.go b/services/room-service/internal/storage/mysql/room_decoration_assignment.go new file mode 100644 index 00000000..b8363fdd --- /dev/null +++ b/services/room-service/internal/storage/mysql/room_decoration_assignment.go @@ -0,0 +1,79 @@ +package mysql + +import ( + "context" + "database/sql" + "errors" + "strings" + + "hyapp/pkg/appcode" + roomservice "hyapp/services/room-service/internal/room/service" +) + +// ListRoomDecorationAssignments 只扫描装扮专用反向索引,并使用稳定 room_id/type 游标分页。 +// resource/user 事件分别命中对应复合索引;VipProgramChanged 才会按主键遍历该低频小表。 +func (r *Repository) ListRoomDecorationAssignments(ctx context.Context, query roomservice.RoomDecorationAssignmentQuery) ([]roomservice.RoomDecorationAssignment, error) { + limit := query.Limit + if limit <= 0 || limit > 500 { + limit = 100 + } + args := []any{appcode.FromContext(ctx)} + statement := `SELECT app_code, room_id, decoration_type, owner_user_id, benefit_code, + resource_id, entitlement_id, expires_at_ms, updated_at_ms + FROM room_decoration_assignments WHERE app_code = ?` + switch { + case query.ResourceID > 0: + statement += ` AND resource_id = ?` + args = append(args, query.ResourceID) + case query.OwnerUserID > 0: + statement += ` AND owner_user_id = ?` + args = append(args, query.OwnerUserID) + } + if strings.TrimSpace(query.AfterRoomID) != "" { + statement += ` AND (room_id > ? OR (room_id = ? AND decoration_type > ?))` + args = append(args, strings.TrimSpace(query.AfterRoomID), strings.TrimSpace(query.AfterRoomID), strings.TrimSpace(query.AfterDecorationType)) + } + statement += ` ORDER BY room_id ASC, decoration_type ASC LIMIT ?` + args = append(args, limit) + rows, err := r.db.QueryContext(ctx, statement, args...) + if err != nil { + return nil, err + } + defer rows.Close() + assignments := make([]roomservice.RoomDecorationAssignment, 0, limit) + for rows.Next() { + var assignment roomservice.RoomDecorationAssignment + if err := rows.Scan( + &assignment.AppCode, &assignment.RoomID, &assignment.DecorationType, &assignment.OwnerUserID, + &assignment.BenefitCode, &assignment.ResourceID, &assignment.EntitlementID, + &assignment.ExpiresAtMS, &assignment.UpdatedAtMS, + ); err != nil { + return nil, err + } + assignments = append(assignments, assignment) + } + return assignments, rows.Err() +} + +func (r *Repository) HasConsumedWalletDecorationEvent(ctx context.Context, eventID string) (bool, error) { + var value int + err := r.db.QueryRowContext(ctx, ` + SELECT 1 FROM room_wallet_event_consumption + WHERE app_code = ? AND event_id = ? LIMIT 1`, + appcode.FromContext(ctx), strings.TrimSpace(eventID), + ).Scan(&value) + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return err == nil, err +} + +// MarkWalletDecorationEventConsumed 只在该事件所有目标房间均已幂等提交后调用。 +func (r *Repository) MarkWalletDecorationEventConsumed(ctx context.Context, eventID string, eventType string, consumedAtMS int64) error { + _, err := r.db.ExecContext(ctx, ` + INSERT INTO room_wallet_event_consumption (app_code, event_id, event_type, consumed_at_ms) + VALUES (?, ?, ?, ?) + ON DUPLICATE KEY UPDATE event_id = room_wallet_event_consumption.event_id`, + appcode.FromContext(ctx), strings.TrimSpace(eventID), strings.TrimSpace(eventType), consumedAtMS) + return err +} diff --git a/services/room-service/internal/storage/mysql/room_list.go b/services/room-service/internal/storage/mysql/room_list.go index 77358b80..35572f40 100644 --- a/services/room-service/internal/storage/mysql/room_list.go +++ b/services/room-service/internal/storage/mysql/room_list.go @@ -20,8 +20,8 @@ func (r *Repository) UpsertRoomListEntry(ctx context.Context, entry roomservice. _, err := r.db.ExecContext(ctx, `INSERT INTO room_list_entries ( app_code, room_id, room_short_id, visible_region_id, owner_country_code, owner_user_id, title, cover_url, mode, status, locked, - heat, online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + heat, online_count, seat_count, occupied_seat_count, sort_score, room_border_json, room_name_style_json, created_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULLIF(?, ''), NULLIF(?, ''), ?, ?) ON DUPLICATE KEY UPDATE room_short_id = VALUES(room_short_id), visible_region_id = VALUES(visible_region_id), @@ -37,6 +37,8 @@ func (r *Repository) UpsertRoomListEntry(ctx context.Context, entry roomservice. seat_count = VALUES(seat_count), occupied_seat_count = VALUES(occupied_seat_count), sort_score = VALUES(sort_score), + room_border_json = VALUES(room_border_json), + room_name_style_json = VALUES(room_name_style_json), updated_at_ms = VALUES(updated_at_ms)`, appCode, entry.RoomID, @@ -54,6 +56,8 @@ func (r *Repository) UpsertRoomListEntry(ctx context.Context, entry roomservice. entry.SeatCount, entry.OccupiedSeatCount, entry.SortScore, + entry.RoomBorderJSON, + entry.RoomNameStyleJSON, entry.CreatedAtMS, entry.UpdatedAtMS, ) @@ -80,32 +84,8 @@ func (r *Repository) ListRoomListEntries(ctx context.Context, query roomservice. entries := make([]roomservice.RoomListEntry, 0, query.Limit) for rows.Next() { - var entry roomservice.RoomListEntry - var pinned int64 - if err := rows.Scan( - &entry.AppCode, - &entry.RoomID, - &entry.RoomShortID, - &entry.VisibleRegionID, - &entry.OwnerCountryCode, - &entry.OwnerUserID, - &entry.Title, - &entry.CoverURL, - &entry.Mode, - &entry.Status, - &entry.Locked, - &entry.Heat, - &entry.OnlineCount, - &entry.SeatCount, - &entry.OccupiedSeatCount, - &entry.SortScore, - &entry.CreatedAtMS, - &entry.UpdatedAtMS, - &pinned, - &entry.PinListRank, - &entry.PinWeight, - &entry.PinnedUntilMS, - ); err != nil { + entry, pinned, err := scanRoomListEntryWithPins(rows) + if err != nil { return nil, err } @@ -136,7 +116,8 @@ func (r *Repository) ListRoomListEntriesByIDs(ctx context.Context, query roomser } rows, err := r.db.QueryContext(ctx, ` SELECT app_code, room_id, room_short_id, visible_region_id, owner_country_code, owner_user_id, title, cover_url, mode, status, locked, - heat, online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms + heat, online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms, + COALESCE(CAST(room_border_json AS CHAR), ''), COALESCE(CAST(room_name_style_json AS CHAR), '') FROM room_list_entries WHERE app_code = ? AND room_id IN (`+placeholders+`)`, args...) if err != nil { @@ -145,27 +126,8 @@ func (r *Repository) ListRoomListEntriesByIDs(ctx context.Context, query roomser defer rows.Close() for rows.Next() { - var entry roomservice.RoomListEntry - if err := rows.Scan( - &entry.AppCode, - &entry.RoomID, - &entry.RoomShortID, - &entry.VisibleRegionID, - &entry.OwnerCountryCode, - &entry.OwnerUserID, - &entry.Title, - &entry.CoverURL, - &entry.Mode, - &entry.Status, - &entry.Locked, - &entry.Heat, - &entry.OnlineCount, - &entry.SeatCount, - &entry.OccupiedSeatCount, - &entry.SortScore, - &entry.CreatedAtMS, - &entry.UpdatedAtMS, - ); err != nil { + entry, err := scanRoomListEntry(rows) + if err != nil { return nil, err } result[entry.RoomID] = entry @@ -251,7 +213,7 @@ func (r *Repository) ListRoomListCachePins(ctx context.Context, query roomservic func scanRoomListEntry(scanner interface{ Scan(dest ...any) error }) (roomservice.RoomListEntry, error) { var entry roomservice.RoomListEntry - err := scanner.Scan( + dest := []any{ &entry.AppCode, &entry.RoomID, &entry.RoomShortID, @@ -270,10 +232,43 @@ func scanRoomListEntry(scanner interface{ Scan(dest ...any) error }) (roomservic &entry.SortScore, &entry.CreatedAtMS, &entry.UpdatedAtMS, - ) + } + if scannerColumnCount(scanner) >= len(dest)+2 { + dest = append(dest, &entry.RoomBorderJSON, &entry.RoomNameStyleJSON) + } + err := scanner.Scan(dest...) return entry, err } +func scanRoomListEntryWithPins(scanner interface{ Scan(dest ...any) error }) (roomservice.RoomListEntry, int64, error) { + var entry roomservice.RoomListEntry + var pinned int64 + dest := []any{ + &entry.AppCode, &entry.RoomID, &entry.RoomShortID, &entry.VisibleRegionID, &entry.OwnerCountryCode, + &entry.OwnerUserID, &entry.Title, &entry.CoverURL, &entry.Mode, &entry.Status, &entry.Locked, + &entry.Heat, &entry.OnlineCount, &entry.SeatCount, &entry.OccupiedSeatCount, &entry.SortScore, + &entry.CreatedAtMS, &entry.UpdatedAtMS, + } + if scannerColumnCount(scanner) >= len(dest)+2+4 { + dest = append(dest, &entry.RoomBorderJSON, &entry.RoomNameStyleJSON) + } + dest = append(dest, &pinned, &entry.PinListRank, &entry.PinWeight, &entry.PinnedUntilMS) + err := scanner.Scan(dest...) + return entry, pinned, err +} + +func scannerColumnCount(scanner any) int { + withColumns, ok := scanner.(interface{ Columns() ([]string, error) }) + if !ok { + return 0 + } + columns, err := withColumns.Columns() + if err != nil { + return 0 + } + return len(columns) +} + func uniqueRoomListIDs(values []string) []string { seen := make(map[string]struct{}, len(values)) out := make([]string, 0, len(values)) @@ -326,27 +321,8 @@ func (r *Repository) ListRoomUserFeedEntries(ctx context.Context, query roomserv entries := make([]roomservice.RoomListEntry, 0, query.Limit) for rows.Next() { - var entry roomservice.RoomListEntry - if err := rows.Scan( - &entry.AppCode, - &entry.RoomID, - &entry.RoomShortID, - &entry.VisibleRegionID, - &entry.OwnerCountryCode, - &entry.OwnerUserID, - &entry.Title, - &entry.CoverURL, - &entry.Mode, - &entry.Status, - &entry.Locked, - &entry.Heat, - &entry.OnlineCount, - &entry.SeatCount, - &entry.OccupiedSeatCount, - &entry.SortScore, - &entry.CreatedAtMS, - &entry.UpdatedAtMS, - ); err != nil { + entry, err := scanRoomListEntry(rows) + if err != nil { return nil, err } @@ -452,27 +428,8 @@ func (r *Repository) ListRoomFollowEntries(ctx context.Context, query roomservic entries := make([]roomservice.RoomListEntry, 0, query.Limit) for rows.Next() { - var entry roomservice.RoomListEntry - if err := rows.Scan( - &entry.AppCode, - &entry.RoomID, - &entry.RoomShortID, - &entry.VisibleRegionID, - &entry.OwnerCountryCode, - &entry.OwnerUserID, - &entry.Title, - &entry.CoverURL, - &entry.Mode, - &entry.Status, - &entry.Locked, - &entry.Heat, - &entry.OnlineCount, - &entry.SeatCount, - &entry.OccupiedSeatCount, - &entry.SortScore, - &entry.CreatedAtMS, - &entry.UpdatedAtMS, - ); err != nil { + entry, err := scanRoomListEntry(rows) + if err != nil { return nil, err } @@ -505,27 +462,8 @@ func (r *Repository) ListRoomRelatedFeedEntries(ctx context.Context, query rooms entries := make([]roomservice.RoomListEntry, 0, query.Limit) for rows.Next() { - var entry roomservice.RoomListEntry - if err := rows.Scan( - &entry.AppCode, - &entry.RoomID, - &entry.RoomShortID, - &entry.VisibleRegionID, - &entry.OwnerCountryCode, - &entry.OwnerUserID, - &entry.Title, - &entry.CoverURL, - &entry.Mode, - &entry.Status, - &entry.Locked, - &entry.Heat, - &entry.OnlineCount, - &entry.SeatCount, - &entry.OccupiedSeatCount, - &entry.SortScore, - &entry.CreatedAtMS, - &entry.UpdatedAtMS, - ); err != nil { + entry, err := scanRoomListEntry(rows) + if err != nil { return nil, err } @@ -556,25 +494,29 @@ func (r *Repository) ListRoomRelatedFeedEntries(ctx context.Context, query rooms const roomListSelectColumns = ` SELECT app_code, room_id, room_short_id, visible_region_id, owner_country_code, owner_user_id, title, cover_url, mode, status, locked, - heat, online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms + heat, online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms, + COALESCE(CAST(room_border_json AS CHAR), ''), COALESCE(CAST(room_name_style_json AS CHAR), '') FROM room_list_entries` // owner 定向列表复用公共列表扫描器,但不参与公共发现页置顶排序;这里补零值列保持 scan 契约稳定。 const roomListOwnerSelectColumns = ` SELECT app_code, room_id, room_short_id, visible_region_id, owner_country_code, owner_user_id, title, cover_url, mode, status, locked, heat, online_count, seat_count, occupied_seat_count, sort_score, created_at_ms, updated_at_ms, + COALESCE(CAST(room_border_json AS CHAR), ''), COALESCE(CAST(room_name_style_json AS CHAR), ''), 0 AS is_pinned, 0 AS pin_list_rank, 0 AS pin_weight, 0 AS pinned_until_ms FROM room_list_entries` const roomUserFeedSelectColumns = ` SELECT r.app_code, r.room_id, r.room_short_id, r.visible_region_id, r.owner_country_code, r.owner_user_id, r.title, r.cover_url, r.mode, r.status, r.locked, - r.heat, r.online_count, r.seat_count, r.occupied_seat_count, r.sort_score, r.created_at_ms, f.updated_at_ms + r.heat, r.online_count, r.seat_count, r.occupied_seat_count, r.sort_score, r.created_at_ms, f.updated_at_ms, + COALESCE(CAST(r.room_border_json AS CHAR), ''), COALESCE(CAST(r.room_name_style_json AS CHAR), '') FROM room_user_feed_entries f JOIN room_list_entries r ON r.app_code = f.app_code AND r.room_id = f.room_id` const roomFollowSelectColumns = ` SELECT r.app_code, r.room_id, r.room_short_id, r.visible_region_id, r.owner_country_code, r.owner_user_id, r.title, r.cover_url, r.mode, r.status, r.locked, - r.heat, r.online_count, r.seat_count, r.occupied_seat_count, r.sort_score, r.created_at_ms, f.followed_at_ms + r.heat, r.online_count, r.seat_count, r.occupied_seat_count, r.sort_score, r.created_at_ms, f.followed_at_ms, + COALESCE(CAST(r.room_border_json AS CHAR), ''), COALESCE(CAST(r.room_name_style_json AS CHAR), '') FROM room_follows f JOIN room_list_entries r ON r.app_code = f.app_code AND r.room_id = f.room_id` @@ -616,6 +558,7 @@ func buildRoomListQuerySQL(query roomservice.RoomListQuery) (string, []any) { selectSQL := ` SELECT r.app_code, r.room_id, r.room_short_id, r.visible_region_id, r.owner_country_code, r.owner_user_id, r.title, r.cover_url, r.mode, r.status, r.locked, r.heat, r.online_count, r.seat_count, r.occupied_seat_count, r.sort_score, r.created_at_ms, r.updated_at_ms, + COALESCE(CAST(r.room_border_json AS CHAR), ''), COALESCE(CAST(r.room_name_style_json AS CHAR), ''), ` + pinnedExpr + ` AS is_pinned, ` + pinRankExpr + ` AS pin_list_rank, ` + pinWeightExpr + ` AS pin_weight, ` + pinExpiresExpr + ` AS pinned_until_ms FROM room_list_entries r LEFT JOIN room_region_pins gp diff --git a/services/room-service/internal/storage/mysql/room_media_upload.go b/services/room-service/internal/storage/mysql/room_media_upload.go new file mode 100644 index 00000000..db48986f --- /dev/null +++ b/services/room-service/internal/storage/mysql/room_media_upload.go @@ -0,0 +1,168 @@ +package mysql + +import ( + "context" + "database/sql" + "errors" + "strings" + "time" + + "hyapp/pkg/appcode" + roomservice "hyapp/services/room-service/internal/room/service" +) + +// GetRoomMediaUploadRegistration 按上传 command_id 的主键读取首次授权事实。 +// 该查询只命中 PRIMARY KEY,不扫描媒体表,可安全放在每次专用上传的权限检查之前。 +func (r *Repository) GetRoomMediaUploadRegistration(ctx context.Context, roomID string, commandID string) (roomservice.RoomMediaUploadRegistration, bool, error) { + row := r.db.QueryRowContext(ctx, ` + SELECT app_code, room_id, command_id, actor_user_id, purpose, media_payload_sha256, + expected_object_key, object_url, status, content_type, media_format, size_bytes, + width, height, animated, frame_count, duration_ms, sha256, + authorized_room_version, created_at_ms, updated_at_ms + FROM room_media_upload_registrations + WHERE app_code = ? AND room_id = ? AND command_id = ? + LIMIT 1`, appcode.FromContext(ctx), strings.TrimSpace(roomID), strings.TrimSpace(commandID)) + registration, err := scanRoomMediaUploadRegistration(row) + if errors.Is(err, sql.ErrNoRows) { + return roomservice.RoomMediaUploadRegistration{}, false, nil + } + return registration, err == nil, err +} + +// GetActiveRoomMediaUploadByObjectKey 是 Save/Send 接受媒体事实前的权威反查。 +// expected_object_key 有唯一索引,查询不会按 URL 或 JSON 字段扫描。 +func (r *Repository) GetActiveRoomMediaUploadByObjectKey(ctx context.Context, objectKey string) (roomservice.RoomMediaUploadRegistration, bool, error) { + row := r.db.QueryRowContext(ctx, ` + SELECT app_code, room_id, command_id, actor_user_id, purpose, media_payload_sha256, + expected_object_key, object_url, status, content_type, media_format, size_bytes, + width, height, animated, frame_count, duration_ms, sha256, + authorized_room_version, created_at_ms, updated_at_ms + FROM room_media_upload_registrations + WHERE app_code = ? AND expected_object_key = ? AND status = 'active' + LIMIT 1`, appcode.FromContext(ctx), strings.TrimLeft(strings.TrimSpace(objectKey), "/")) + registration, err := scanRoomMediaUploadRegistration(row) + if errors.Is(err, sql.ErrNoRows) { + return roomservice.RoomMediaUploadRegistration{}, false, nil + } + return registration, err == nil, err +} + +// RegisterRoomMediaUpload 在首次授权通过后原子占用 command_id。 +// 并发重复通过唯一主键收敛到首行,再逐字段比较;不同文件、用户或用途不能被 ON DUPLICATE 静默吞掉。 +func (r *Repository) RegisterRoomMediaUpload(ctx context.Context, registration roomservice.RoomMediaUploadRegistration) (roomservice.RoomMediaUploadRegistration, error) { + registration.AppCode = appcode.FromContext(ctx) + registration.RoomID = strings.TrimSpace(registration.RoomID) + registration.CommandID = strings.TrimSpace(registration.CommandID) + registration.Purpose = strings.TrimSpace(registration.Purpose) + registration.MediaPayloadSHA256 = strings.ToLower(strings.TrimSpace(registration.MediaPayloadSHA256)) + registration.ExpectedObjectKey = strings.TrimLeft(strings.TrimSpace(registration.ExpectedObjectKey), "/") + registration.ObjectURL = "" + registration.Status = "authorized" + if registration.CreatedAtMS <= 0 { + registration.CreatedAtMS = time.Now().UTC().UnixMilli() + } + if registration.UpdatedAtMS <= 0 { + registration.UpdatedAtMS = registration.CreatedAtMS + } + _, err := r.db.ExecContext(ctx, ` + INSERT INTO room_media_upload_registrations ( + app_code, room_id, command_id, actor_user_id, purpose, media_payload_sha256, + expected_object_key, object_url, status, content_type, media_format, size_bytes, + width, height, animated, frame_count, duration_ms, sha256, + authorized_room_version, created_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, '', 'authorized', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE command_id = room_media_upload_registrations.command_id`, + registration.AppCode, registration.RoomID, registration.CommandID, registration.ActorUserID, + registration.Purpose, registration.MediaPayloadSHA256, registration.ExpectedObjectKey, + registration.Media.ContentType, registration.Media.Format, registration.Media.SizeBytes, + registration.Media.Width, registration.Media.Height, registration.Media.Animated, + registration.Media.FrameCount, registration.Media.DurationMS, registration.Media.SHA256, registration.AuthorizedVersion, + registration.CreatedAtMS, registration.UpdatedAtMS) + if err != nil { + return roomservice.RoomMediaUploadRegistration{}, err + } + saved, exists, err := r.GetRoomMediaUploadRegistration(ctx, registration.RoomID, registration.CommandID) + if err != nil { + return roomservice.RoomMediaUploadRegistration{}, err + } + if !exists || !sameRoomMediaUploadRegistration(saved, registration) { + return roomservice.RoomMediaUploadRegistration{}, roomservice.ErrRoomMediaUploadCommandConflict + } + return saved, nil +} + +// CompleteRoomMediaUpload 只把首次授权的 expected_object_key 推进为 active。 +// URL 和完整媒体事实必须先由 service 与授权记录逐字段匹配;并发 complete 仍以首个 URL 为准。 +func (r *Repository) CompleteRoomMediaUpload(ctx context.Context, registration roomservice.RoomMediaUploadRegistration) (roomservice.RoomMediaUploadRegistration, error) { + registration.AppCode = appcode.FromContext(ctx) + registration.RoomID = strings.TrimSpace(registration.RoomID) + registration.CommandID = strings.TrimSpace(registration.CommandID) + registration.ExpectedObjectKey = strings.TrimLeft(strings.TrimSpace(registration.ExpectedObjectKey), "/") + registration.ObjectURL = strings.TrimSpace(registration.ObjectURL) + registration.Status = "active" + if registration.UpdatedAtMS <= 0 { + registration.UpdatedAtMS = time.Now().UTC().UnixMilli() + } + _, err := r.db.ExecContext(ctx, ` + UPDATE room_media_upload_registrations + SET object_url = ?, status = 'active', updated_at_ms = ? + WHERE app_code = ? AND room_id = ? AND command_id = ? AND status = 'authorized'`, + registration.ObjectURL, registration.UpdatedAtMS, registration.AppCode, registration.RoomID, registration.CommandID) + if err != nil { + return roomservice.RoomMediaUploadRegistration{}, err + } + saved, exists, err := r.GetRoomMediaUploadRegistration(ctx, registration.RoomID, registration.CommandID) + if err != nil { + return roomservice.RoomMediaUploadRegistration{}, err + } + if !exists || !sameRoomMediaUploadRegistration(saved, registration) || + saved.Status != "active" || strings.TrimSpace(saved.ObjectURL) != registration.ObjectURL { + return roomservice.RoomMediaUploadRegistration{}, roomservice.ErrRoomMediaUploadCommandConflict + } + return saved, nil +} + +func scanRoomMediaUploadRegistration(scanner interface{ Scan(dest ...any) error }) (roomservice.RoomMediaUploadRegistration, error) { + var registration roomservice.RoomMediaUploadRegistration + err := scanner.Scan( + ®istration.AppCode, + ®istration.RoomID, + ®istration.CommandID, + ®istration.ActorUserID, + ®istration.Purpose, + ®istration.MediaPayloadSHA256, + ®istration.ExpectedObjectKey, + ®istration.ObjectURL, + ®istration.Status, + ®istration.Media.ContentType, + ®istration.Media.Format, + ®istration.Media.SizeBytes, + ®istration.Media.Width, + ®istration.Media.Height, + ®istration.Media.Animated, + ®istration.Media.FrameCount, + ®istration.Media.DurationMS, + ®istration.Media.SHA256, + ®istration.AuthorizedVersion, + ®istration.CreatedAtMS, + ®istration.UpdatedAtMS, + ) + if err == nil { + registration.Media.ObjectKey = registration.ExpectedObjectKey + registration.Media.URL = registration.ObjectURL + if registration.Status == "active" { + registration.Media.Status = "active" + } + } + return registration, err +} + +func sameRoomMediaUploadRegistration(left roomservice.RoomMediaUploadRegistration, right roomservice.RoomMediaUploadRegistration) bool { + return appcode.Normalize(left.AppCode) == appcode.Normalize(right.AppCode) && + strings.TrimSpace(left.RoomID) == strings.TrimSpace(right.RoomID) && + strings.TrimSpace(left.CommandID) == strings.TrimSpace(right.CommandID) && + left.ActorUserID == right.ActorUserID && + strings.TrimSpace(left.Purpose) == strings.TrimSpace(right.Purpose) && + strings.EqualFold(strings.TrimSpace(left.MediaPayloadSHA256), strings.TrimSpace(right.MediaPayloadSHA256)) && + strings.TrimLeft(strings.TrimSpace(left.ExpectedObjectKey), "/") == strings.TrimLeft(strings.TrimSpace(right.ExpectedObjectKey), "/") +} diff --git a/services/room-service/internal/storage/mysql/room_meta.go b/services/room-service/internal/storage/mysql/room_meta.go index 13862e6e..cc4f7c96 100644 --- a/services/room-service/internal/storage/mysql/room_meta.go +++ b/services/room-service/internal/storage/mysql/room_meta.go @@ -129,7 +129,8 @@ func (r *Repository) GetRoomMetaByOwner(ctx context.Context, ownerUserID int64) return meta, true, nil } -// SaveRoomBackground 保存房间背景图素材;相同房间相同 URL 按幂等返回原记录。 +// SaveRoomBackground 保存房间背景图素材。command_id 是素材保存幂等键;URL 唯一键只负责去重, +// 不能把相同 command_id 的另一份媒体悄悄折叠成旧记录。 func (r *Repository) SaveRoomBackground(ctx context.Context, background roomservice.RoomBackgroundImage) (roomservice.RoomBackgroundImage, error) { appCode := normalizedRecordAppCode(ctx, background.AppCode) roomID := strings.TrimSpace(background.RoomID) @@ -139,29 +140,53 @@ func (r *Repository) SaveRoomBackground(ctx context.Context, background roomserv nowMS = time.Now().UTC().UnixMilli() } + if existing, exists, err := r.getRoomBackgroundByCommand(ctx, appCode, roomID, background.CommandID); err != nil { + return roomservice.RoomBackgroundImage{}, err + } else if exists { + if !sameRoomBackgroundMedia(existing, background) { + return roomservice.RoomBackgroundImage{}, roomservice.ErrRoomBackgroundCommandConflict + } + return existing, nil + } + if strings.TrimSpace(background.Media.ObjectKey) == "" { + // legacy_timed 以 URL 作为历史幂等键;迁移前的行没有 command_id/media,重复保存必须继续返回原素材。 + if existing, exists, err := r.getRoomBackgroundByURL(ctx, appCode, roomID, imageURL); err != nil { + return roomservice.RoomBackgroundImage{}, err + } else if exists { + return existing, nil + } + } + _, err := r.db.ExecContext(ctx, ` INSERT INTO room_background_images ( - app_code, room_id, image_url, created_by_user_id, created_at_ms, updated_at_ms - ) VALUES (?, ?, ?, ?, ?, ?) - ON DUPLICATE KEY UPDATE updated_at_ms = VALUES(updated_at_ms) - `, appCode, roomID, imageURL, background.CreatedByUserID, nowMS, nowMS) + app_code, room_id, image_url, command_id, object_key, content_type, media_format, + size_bytes, width, height, animated, frame_count, duration_ms, poster_url, + thumbnail_url, sha256, media_status, created_by_user_id, created_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE background_id = LAST_INSERT_ID(background_id) + `, appCode, roomID, imageURL, strings.TrimSpace(background.CommandID), background.Media.ObjectKey, + background.Media.ContentType, background.Media.Format, background.Media.SizeBytes, background.Media.Width, + background.Media.Height, background.Media.Animated, background.Media.FrameCount, background.Media.DurationMS, + background.Media.PosterURL, background.Media.ThumbnailURL, background.Media.SHA256, background.Media.Status, + background.CreatedByUserID, nowMS, nowMS) if err != nil { return roomservice.RoomBackgroundImage{}, err } - row := r.db.QueryRowContext(ctx, ` - SELECT app_code, background_id, room_id, image_url, created_by_user_id, created_at_ms, updated_at_ms - FROM room_background_images - WHERE app_code = ? AND room_id = ? AND image_url = ? - LIMIT 1 - `, appCode, roomID, imageURL) - return scanRoomBackground(row) + saved, exists, err := r.getRoomBackgroundByCommand(ctx, appCode, roomID, background.CommandID) + if err != nil { + return roomservice.RoomBackgroundImage{}, err + } + if !exists || !sameRoomBackgroundMedia(saved, background) { + return roomservice.RoomBackgroundImage{}, roomservice.ErrRoomBackgroundCommandConflict + } + return saved, nil } // GetRoomBackground 精确读取一个房间背景图素材。 func (r *Repository) GetRoomBackground(ctx context.Context, roomID string, backgroundID int64) (roomservice.RoomBackgroundImage, bool, error) { row := r.db.QueryRowContext(ctx, ` - SELECT app_code, background_id, room_id, image_url, created_by_user_id, created_at_ms, updated_at_ms + SELECT `+roomBackgroundSelectColumns+` FROM room_background_images WHERE app_code = ? AND room_id = ? AND background_id = ? LIMIT 1 @@ -177,10 +202,16 @@ func (r *Repository) GetRoomBackground(ctx context.Context, roomID string, backg return background, true, nil } +// GetRoomBackgroundByCommand 供 service 在任何可变权限/房态检查之前重放已保存素材。 +// command_id 唯一索引保证最多一行;app_code 继续从已验签 RequestMeta context 读取,避免跨租户重放。 +func (r *Repository) GetRoomBackgroundByCommand(ctx context.Context, roomID string, commandID string) (roomservice.RoomBackgroundImage, bool, error) { + return r.getRoomBackgroundByCommand(ctx, appcode.FromContext(ctx), strings.TrimSpace(roomID), strings.TrimSpace(commandID)) +} + // ListRoomBackgrounds 返回某个房间保存过的背景图素材,按最近保存优先。 func (r *Repository) ListRoomBackgrounds(ctx context.Context, roomID string) ([]roomservice.RoomBackgroundImage, error) { rows, err := r.db.QueryContext(ctx, ` - SELECT app_code, background_id, room_id, image_url, created_by_user_id, created_at_ms, updated_at_ms + SELECT `+roomBackgroundSelectColumns+` FROM room_background_images WHERE app_code = ? AND room_id = ? ORDER BY created_at_ms DESC, background_id DESC @@ -208,9 +239,52 @@ func scanRoomBackground(scanner interface{ Scan(dest ...any) error }) (roomservi &background.BackgroundID, &background.RoomID, &background.ImageURL, + &background.CommandID, + &background.Media.ObjectKey, + &background.Media.ContentType, + &background.Media.Format, + &background.Media.SizeBytes, + &background.Media.Width, + &background.Media.Height, + &background.Media.Animated, + &background.Media.FrameCount, + &background.Media.DurationMS, + &background.Media.PosterURL, + &background.Media.ThumbnailURL, + &background.Media.SHA256, + &background.Media.Status, &background.CreatedByUserID, &background.CreatedAtMS, &background.UpdatedAtMS, ) return background, err } + +const roomBackgroundSelectColumns = `app_code, background_id, room_id, image_url, COALESCE(command_id, ''), + object_key, content_type, media_format, size_bytes, width, height, animated, frame_count, + duration_ms, poster_url, thumbnail_url, sha256, media_status, created_by_user_id, created_at_ms, updated_at_ms` + +func (r *Repository) getRoomBackgroundByCommand(ctx context.Context, appCode string, roomID string, commandID string) (roomservice.RoomBackgroundImage, bool, error) { + row := r.db.QueryRowContext(ctx, `SELECT `+roomBackgroundSelectColumns+` + FROM room_background_images WHERE app_code = ? AND room_id = ? AND command_id = ? LIMIT 1`, appCode, roomID, strings.TrimSpace(commandID)) + background, err := scanRoomBackground(row) + if errors.Is(err, sql.ErrNoRows) { + return roomservice.RoomBackgroundImage{}, false, nil + } + return background, err == nil, err +} + +func (r *Repository) getRoomBackgroundByURL(ctx context.Context, appCode string, roomID string, imageURL string) (roomservice.RoomBackgroundImage, bool, error) { + row := r.db.QueryRowContext(ctx, `SELECT `+roomBackgroundSelectColumns+` + FROM room_background_images WHERE app_code = ? AND room_id = ? AND image_url = ? LIMIT 1`, appCode, roomID, strings.TrimSpace(imageURL)) + background, err := scanRoomBackground(row) + if errors.Is(err, sql.ErrNoRows) { + return roomservice.RoomBackgroundImage{}, false, nil + } + return background, err == nil, err +} + +func sameRoomBackgroundMedia(left roomservice.RoomBackgroundImage, right roomservice.RoomBackgroundImage) bool { + return strings.TrimSpace(left.ImageURL) == strings.TrimSpace(right.ImageURL) && + left.CreatedByUserID == right.CreatedByUserID && left.Media == right.Media +} diff --git a/services/room-service/internal/storage/mysql/schema.go b/services/room-service/internal/storage/mysql/schema.go index c7061058..20ac04ad 100644 --- a/services/room-service/internal/storage/mysql/schema.go +++ b/services/room-service/internal/storage/mysql/schema.go @@ -51,6 +51,8 @@ func (r *Repository) Migrate(ctx context.Context) error { seat_count INT NOT NULL DEFAULT 0, occupied_seat_count INT NOT NULL DEFAULT 0, sort_score BIGINT NOT NULL DEFAULT 0, + room_border_json JSON NULL, + room_name_style_json JSON NULL, created_at_ms BIGINT NOT NULL, updated_at_ms BIGINT NOT NULL, PRIMARY KEY (app_code, room_id), @@ -107,14 +109,78 @@ func (r *Repository) Migrate(ctx context.Context) error { app_code VARCHAR(32) NOT NULL, background_id BIGINT NOT NULL AUTO_INCREMENT, room_id VARCHAR(64) NOT NULL, - image_url VARCHAR(256) NOT NULL, + image_url VARCHAR(1024) NOT NULL, + command_id VARCHAR(128) NULL, + object_key VARCHAR(512) NOT NULL DEFAULT '', + content_type VARCHAR(64) NOT NULL DEFAULT '', + media_format VARCHAR(16) NOT NULL DEFAULT '', + size_bytes BIGINT NOT NULL DEFAULT 0, + width INT NOT NULL DEFAULT 0, + height INT NOT NULL DEFAULT 0, + animated BOOLEAN NOT NULL DEFAULT FALSE, + frame_count INT NOT NULL DEFAULT 0, + duration_ms BIGINT NOT NULL DEFAULT 0, + poster_url VARCHAR(1024) NOT NULL DEFAULT '', + thumbnail_url VARCHAR(1024) NOT NULL DEFAULT '', + sha256 CHAR(64) NOT NULL DEFAULT '', + media_status VARCHAR(32) NOT NULL DEFAULT '', created_by_user_id BIGINT NOT NULL, created_at_ms BIGINT NOT NULL, updated_at_ms BIGINT NOT NULL, PRIMARY KEY (background_id), UNIQUE KEY uk_room_background_url (app_code, room_id, image_url), + UNIQUE KEY uk_room_background_command (app_code, room_id, command_id), KEY idx_room_background_list (app_code, room_id, created_at_ms DESC, background_id DESC) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, + `CREATE TABLE IF NOT EXISTS room_media_upload_registrations ( + app_code VARCHAR(32) NOT NULL, + room_id VARCHAR(64) NOT NULL, + command_id VARCHAR(128) NOT NULL, + actor_user_id BIGINT NOT NULL, + purpose VARCHAR(32) NOT NULL, + media_payload_sha256 CHAR(64) NOT NULL, + expected_object_key VARCHAR(512) NOT NULL, + object_url VARCHAR(1024) NOT NULL DEFAULT '', + status VARCHAR(32) NOT NULL DEFAULT 'authorized', + content_type VARCHAR(64) NOT NULL, + media_format VARCHAR(16) NOT NULL, + size_bytes BIGINT NOT NULL, + width INT NOT NULL, + height INT NOT NULL, + animated BOOLEAN NOT NULL DEFAULT FALSE, + frame_count INT NOT NULL, + duration_ms BIGINT NOT NULL DEFAULT 0, + sha256 CHAR(64) NOT NULL, + authorized_room_version BIGINT NOT NULL, + created_at_ms BIGINT NOT NULL, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, room_id, command_id), + UNIQUE KEY uk_room_media_upload_object (app_code, expected_object_key), + KEY idx_room_media_upload_status_created (app_code, status, created_at_ms) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, + `CREATE TABLE IF NOT EXISTS room_decoration_assignments ( + app_code VARCHAR(32) NOT NULL, + room_id VARCHAR(64) NOT NULL, + decoration_type VARCHAR(32) NOT NULL, + owner_user_id BIGINT NOT NULL, + benefit_code VARCHAR(64) NOT NULL, + resource_id BIGINT NOT NULL, + entitlement_id VARCHAR(128) NOT NULL DEFAULT '', + expires_at_ms BIGINT NOT NULL DEFAULT 0, + updated_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, room_id, decoration_type), + KEY idx_room_decoration_resource (app_code, resource_id, room_id, decoration_type), + KEY idx_room_decoration_owner (app_code, owner_user_id, room_id, decoration_type), + KEY idx_room_decoration_expiry (app_code, expires_at_ms, room_id, decoration_type) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, + `CREATE TABLE IF NOT EXISTS room_wallet_event_consumption ( + app_code VARCHAR(32) NOT NULL, + event_id VARCHAR(128) NOT NULL, + event_type VARCHAR(64) NOT NULL, + consumed_at_ms BIGINT NOT NULL, + PRIMARY KEY (app_code, event_id), + KEY idx_room_wallet_event_consumed (app_code, consumed_at_ms) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`, `CREATE TABLE IF NOT EXISTS room_snapshots ( app_code VARCHAR(32) NOT NULL, room_id VARCHAR(64) NOT NULL, @@ -404,10 +470,65 @@ func (r *Repository) Migrate(ctx context.Context) error { if err := r.ensureGiftOperationSchema(ctx); err != nil { return err } + if err := r.ensureRoomVIPMediaSchema(ctx); err != nil { + return err + } return r.ensureOutboxRetrySchema(ctx) } +func (r *Repository) ensureRoomVIPMediaSchema(ctx context.Context) error { + backgroundColumns := []struct{ name, ddl string }{ + {"command_id", "ALTER TABLE room_background_images ADD COLUMN command_id VARCHAR(128) NULL"}, + {"object_key", "ALTER TABLE room_background_images ADD COLUMN object_key VARCHAR(512) NOT NULL DEFAULT ''"}, + {"content_type", "ALTER TABLE room_background_images ADD COLUMN content_type VARCHAR(64) NOT NULL DEFAULT ''"}, + {"media_format", "ALTER TABLE room_background_images ADD COLUMN media_format VARCHAR(16) NOT NULL DEFAULT ''"}, + {"size_bytes", "ALTER TABLE room_background_images ADD COLUMN size_bytes BIGINT NOT NULL DEFAULT 0"}, + {"width", "ALTER TABLE room_background_images ADD COLUMN width INT NOT NULL DEFAULT 0"}, + {"height", "ALTER TABLE room_background_images ADD COLUMN height INT NOT NULL DEFAULT 0"}, + {"animated", "ALTER TABLE room_background_images ADD COLUMN animated BOOLEAN NOT NULL DEFAULT FALSE"}, + {"frame_count", "ALTER TABLE room_background_images ADD COLUMN frame_count INT NOT NULL DEFAULT 0"}, + {"duration_ms", "ALTER TABLE room_background_images ADD COLUMN duration_ms BIGINT NOT NULL DEFAULT 0"}, + {"poster_url", "ALTER TABLE room_background_images ADD COLUMN poster_url VARCHAR(1024) NOT NULL DEFAULT ''"}, + {"thumbnail_url", "ALTER TABLE room_background_images ADD COLUMN thumbnail_url VARCHAR(1024) NOT NULL DEFAULT ''"}, + {"sha256", "ALTER TABLE room_background_images ADD COLUMN sha256 CHAR(64) NOT NULL DEFAULT ''"}, + {"media_status", "ALTER TABLE room_background_images ADD COLUMN media_status VARCHAR(32) NOT NULL DEFAULT ''"}, + } + for _, column := range backgroundColumns { + exists, err := r.columnExists(ctx, "room_background_images", column.name) + if err != nil { + return err + } + if !exists { + if _, err := r.db.ExecContext(ctx, column.ddl); err != nil { + return err + } + } + } + for _, column := range []struct{ name, ddl string }{ + {"room_border_json", "ALTER TABLE room_list_entries ADD COLUMN room_border_json JSON NULL"}, + {"room_name_style_json", "ALTER TABLE room_list_entries ADD COLUMN room_name_style_json JSON NULL"}, + } { + exists, err := r.columnExists(ctx, "room_list_entries", column.name) + if err != nil { + return err + } + if !exists { + if _, err := r.db.ExecContext(ctx, column.ddl); err != nil { + return err + } + } + } + hasCommandIndex, err := r.indexExists(ctx, "room_background_images", "uk_room_background_command") + if err != nil { + return err + } + if !hasCommandIndex { + _, err = r.db.ExecContext(ctx, "ALTER TABLE room_background_images ADD UNIQUE KEY uk_room_background_command (app_code, room_id, command_id)") + } + return err +} + func (r *Repository) ensureGiftOperationSchema(ctx context.Context) error { // CREATE TABLE IF NOT EXISTS 不会给历史 room_command_log 补列;auto_migrate 必须显式探测, // 否则新二进制启动后第一笔 GetCommand 会因 result_payload 不存在而直接 SQL 失败。 diff --git a/services/room-service/internal/transport/grpc/server.go b/services/room-service/internal/transport/grpc/server.go index 29f1a09f..f6ce8a54 100644 --- a/services/room-service/internal/transport/grpc/server.go +++ b/services/room-service/internal/transport/grpc/server.go @@ -100,6 +100,36 @@ func (s *Server) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBackg return mapServiceResult(s.svc.SetRoomBackground(ctx, req)) } +func (s *Server) ApplyRoomDecoration(ctx context.Context, req *roomv1.ApplyRoomDecorationRequest) (*roomv1.ApplyRoomDecorationResponse, error) { + ctx = contextWithMetaApp(ctx, req.GetMeta()) + if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.ApplyRoomDecorationResponse, error) { + return client.ApplyRoomDecoration(callCtx, req) + }); forwarded { + return resp, err + } + return mapServiceResult(s.svc.ApplyRoomDecoration(ctx, req)) +} + +func (s *Server) SendRoomImage(ctx context.Context, req *roomv1.SendRoomImageRequest) (*roomv1.SendRoomImageResponse, error) { + ctx = contextWithMetaApp(ctx, req.GetMeta()) + if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SendRoomImageResponse, error) { + return client.SendRoomImage(callCtx, req) + }); forwarded { + return resp, err + } + return mapServiceResult(s.svc.SendRoomImage(ctx, req)) +} + +func (s *Server) AuthorizeRoomMediaUpload(ctx context.Context, req *roomv1.AuthorizeRoomMediaUploadRequest) (*roomv1.AuthorizeRoomMediaUploadResponse, error) { + ctx = contextWithMetaApp(ctx, req.GetMeta()) + return mapServiceResult(s.svc.AuthorizeRoomMediaUpload(ctx, req)) +} + +func (s *Server) CompleteRoomMediaUpload(ctx context.Context, req *roomv1.CompleteRoomMediaUploadRequest) (*roomv1.CompleteRoomMediaUploadResponse, error) { + ctx = contextWithMetaApp(ctx, req.GetMeta()) + return mapServiceResult(s.svc.CompleteRoomMediaUpload(ctx, req)) +} + // JoinRoom 代理到领域服务。 func (s *Server) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) { // JoinRoom 只维护 room-service presence,真实消息连接由腾讯云 IM SDK 处理。 @@ -537,6 +567,13 @@ func (s *Server) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomR return mapServiceResult(s.svc.GetCurrentRoom(ctx, req)) } +// BatchGetUserVoiceRoomPresences 代理到个人资料页上麦状态批量查询。 +func (s *Server) BatchGetUserVoiceRoomPresences(ctx context.Context, req *roomv1.BatchGetUserVoiceRoomPresencesRequest) (*roomv1.BatchGetUserVoiceRoomPresencesResponse, error) { + // 查询先用用户级 presence 定位候选房间,再由 Room Cell 快照收敛状态;gRPC 层不复制权限判断。 + ctx = contextWithMetaApp(ctx, req.GetMeta()) + return mapServiceResult(s.svc.BatchGetUserVoiceRoomPresences(ctx, req)) +} + // GetRoomSnapshot 代理到房间页完整快照只读查询。 func (s *Server) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error) { // 快照查询不刷新 presence,不替代 heartbeat,也不能让未进房用户读取完整房间态。 diff --git a/services/user-service/deploy/mysql/initdb/001_user_service.sql b/services/user-service/deploy/mysql/initdb/001_user_service.sql index 2a7f14f8..63283d6e 100644 --- a/services/user-service/deploy/mysql/initdb/001_user_service.sql +++ b/services/user-service/deploy/mysql/initdb/001_user_service.sql @@ -117,6 +117,20 @@ CREATE TABLE IF NOT EXISTS user_profile_visits ( KEY idx_user_profile_visits_target_time (app_code, target_user_id, last_visited_at_ms) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资料访问表'; +CREATE TABLE IF NOT EXISTS user_profile_anonymous_visits ( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离', + target_user_id BIGINT NOT NULL COMMENT '目标用户 ID', + visitor_user_id BIGINT NOT NULL COMMENT '仅 user-service 内部用于聚合的真实访客 ID,禁止对外返回', + anonymous_visit_id VARCHAR(64) NOT NULL COMMENT '首次匿名访问生成的随机展示 ID,不包含真实用户信息', + visit_count BIGINT NOT NULL DEFAULT 1 COMMENT '匿名阶段累计访问数量', + first_visited_at_ms BIGINT NOT NULL COMMENT '首次匿名访问时间,UTC epoch ms', + last_visited_at_ms BIGINT NOT NULL COMMENT '最后匿名访问时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', + PRIMARY KEY (app_code, target_user_id, visitor_user_id), + UNIQUE KEY uk_user_profile_anonymous_visit_id (app_code, target_user_id, anonymous_visit_id), + KEY idx_user_profile_anonymous_visits_target_time (app_code, target_user_id, last_visited_at_ms, anonymous_visit_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资料匿名访问 owner 表'; + CREATE TABLE IF NOT EXISTS user_follows ( app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离', follower_user_id BIGINT NOT NULL COMMENT '关注人用户 ID', diff --git a/services/user-service/deploy/mysql/migrations/019_user_profile_anonymous_visits.sql b/services/user-service/deploy/mysql/migrations/019_user_profile_anonymous_visits.sql new file mode 100644 index 00000000..1d5fb92b --- /dev/null +++ b/services/user-service/deploy/mysql/migrations/019_user_profile_anonymous_visits.sql @@ -0,0 +1,42 @@ +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_user; + +-- 匿名访问使用独立表,避免修改已有 user_profile_visits 热表主键或回填历史数据: +-- 1. CREATE TABLE 不扫描、复制或锁住旧访问表,迁移成本与旧表数据量无关; +-- 2. 旧表中的所有历史记录继续作为公开访问返回,不会因用户当前 VIP 状态被倒推改写; +-- 3. 主键服务单 target/visitor 的幂等累计,target_time 索引服务目标用户分页,查询不扫全表。 +CREATE TABLE IF NOT EXISTS user_profile_anonymous_visits ( + app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离', + target_user_id BIGINT NOT NULL COMMENT '目标用户 ID', + visitor_user_id BIGINT NOT NULL COMMENT '仅 user-service 内部用于聚合的真实访客 ID,禁止对外返回', + anonymous_visit_id VARCHAR(64) NOT NULL COMMENT '首次匿名访问生成的随机展示 ID,不包含真实用户信息', + visit_count BIGINT NOT NULL DEFAULT 1 COMMENT '匿名阶段累计访问数量', + first_visited_at_ms BIGINT NOT NULL COMMENT '首次匿名访问时间,UTC epoch ms', + last_visited_at_ms BIGINT NOT NULL COMMENT '最后匿名访问时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', + PRIMARY KEY (app_code, target_user_id, visitor_user_id), + UNIQUE KEY uk_user_profile_anonymous_visit_id (app_code, target_user_id, anonymous_visit_id), + KEY idx_user_profile_anonymous_visits_target_time (app_code, target_user_id, last_visited_at_ms, anonymous_visit_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资料匿名访问 owner 表'; + +-- CREATE TABLE IF NOT EXISTS 不会为试运行阶段已存在的表补索引;唯一约束和目标时间分页索引都必须 +-- 独立收敛。两个 DDL 只访问新匿名表,不读取或锁住旧 user_profile_visits 热表;若已有重复随机 ID, +-- 唯一键构建会直接失败,由部署方先审计异常数据,不能静默合并两个匿名身份。 +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_profile_anonymous_visits' AND INDEX_NAME = 'uk_user_profile_anonymous_visit_id') = 0, + 'ALTER TABLE user_profile_anonymous_visits ADD UNIQUE INDEX uk_user_profile_anonymous_visit_id (app_code, target_user_id, anonymous_visit_id), ALGORITHM=INPLACE, LOCK=NONE', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_profile_anonymous_visits' AND INDEX_NAME = 'idx_user_profile_anonymous_visits_target_time') = 0, + 'ALTER TABLE user_profile_anonymous_visits ADD INDEX idx_user_profile_anonymous_visits_target_time (app_code, target_user_id, last_visited_at_ms, anonymous_visit_id), ALGORITHM=INPLACE, LOCK=NONE', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; diff --git a/services/user-service/internal/app/app.go b/services/user-service/internal/app/app.go index 87444a8c..d9fb74ee 100644 --- a/services/user-service/internal/app/app.go +++ b/services/user-service/internal/app/app.go @@ -256,6 +256,7 @@ func New(cfg config.Config) (*App, error) { } var walletConn *grpc.ClientConn var avatarFrameReader cpservice.AvatarFrameReader + var privacyBenefitChecker userservice.PrivacyBenefitChecker if cfg.WalletService.Enabled { // 头像框佩戴事实归 wallet-service;user-service 刷新榜单前必须能读取该快照,否则榜单会缺少双方头像框。 walletConn, err = grpcclient.Dial(cfg.WalletService.Addr, grpcclient.Config{ @@ -278,8 +279,11 @@ func New(cfg config.Config) (*App, error) { _ = mysqlRepo.Close() return nil, err } - // 只把头像框读取能力注入 CP service,避免 CP 领域依赖完整的钱包客户端。 - avatarFrameReader = walletclient.NewGRPC(walletConn) + // 同一个轻量 gRPC adapter 分别以最小接口注入 CP 头像框读模型和 user 隐私门禁, + // 两个领域都不会依赖完整 wallet protobuf client 或复制钱包规则。 + walletClient := walletclient.NewGRPC(walletConn) + avatarFrameReader = walletClient + privacyBenefitChecker = walletClient } // auth service 负责登录、session 和 token;用户主数据和短号事务仍通过各自领域存储完成。 @@ -327,6 +331,7 @@ func New(cfg config.Config) (*App, error) { userservice.WithRoleSummaryRepository(hostRepo), userservice.WithDeviceRepository(deviceRepo), userservice.WithModerationRepository(userRepo), + userservice.WithPrivacyBenefitChecker(privacyBenefitChecker), userservice.WithSessionDenylist(ipDecisionCache, time.Duration(cfg.JWT.AccessTokenTTLSec+60)*time.Second), userservice.WithIMLoginKicker(imLoginKicker), userservice.WithRoomEvictor(roomEvictor), diff --git a/services/user-service/internal/domain/user/user.go b/services/user-service/internal/domain/user/user.go index f47e4cb0..ff727c6a 100644 --- a/services/user-service/internal/domain/user/user.go +++ b/services/user-service/internal/domain/user/user.go @@ -342,11 +342,15 @@ const ( ) // ProfileVisitRecord 是访问某个用户主页的去重记录。 +// 公开访问使用 VisitorUserID;VisitorIdentityHidden=true 时只向上游暴露随机 AnonymousVisitID, +// 真实访客 ID 仅留在 user-service 的 owner 表中用于同一目标下累计次数,绝不能进入协议转换层。 type ProfileVisitRecord struct { - VisitorUserID int64 - TargetUserID int64 - VisitCount int64 - LastVisitedAtMs int64 + VisitorUserID int64 + TargetUserID int64 + VisitCount int64 + LastVisitedAtMs int64 + VisitorIdentityHidden bool + AnonymousVisitID string } // FollowRecord 是单向关注关系。 diff --git a/services/user-service/internal/integration/walletclient/client.go b/services/user-service/internal/integration/walletclient/client.go index 33447ec5..6feb3bd2 100644 --- a/services/user-service/internal/integration/walletclient/client.go +++ b/services/user-service/internal/integration/walletclient/client.go @@ -1,22 +1,47 @@ -// Package walletclient 适配 wallet-service 资源 RPC,供 user-service 构建 CP 读模型。 +// Package walletclient 适配 wallet-service 资源与 VIP 权益 RPC,供 user-service 构建展示读模型并执行隐私门禁。 package walletclient import ( "context" + "strings" "google.golang.org/grpc" walletv1 "hyapp.local/api/proto/wallet/v1" + "hyapp/pkg/appcode" "hyapp/pkg/idgen" cpdomain "hyapp/services/user-service/internal/domain/cp" ) const avatarFrameResourceType = "avatar_frame" -// Client 只暴露 user-service 构建 CP 读模型需要的钱包资源读取能力。 +// Client 只暴露 user-service 构建 CP 读模型和执行资料隐私需要的钱包读取能力。 type Client struct { client walletv1.WalletServiceClient } +// CheckVipBenefit 读取 wallet-service 已结合 App 配置、有效 VIP、体验卡排除项及用户开关计算出的最终结果。 +// user-service 不缓存隐私权益,保证资料统计隐藏和匿名访问每次都按访问发生时的事实固化。 +func (c *Client) CheckVipBenefit(ctx context.Context, requestID string, userID int64, benefitCode string) (bool, error) { + if c == nil || c.client == nil { + return false, grpc.ErrClientConnClosing + } + requestID = strings.TrimSpace(requestID) + if requestID == "" { + // 非 gateway 的内部调用缺 request_id 时生成兜底链路 ID;HTTP 请求始终复用 gateway 已生成的 request_id。 + requestID = idgen.New("user_privacy") + } + resp, err := c.client.CheckVipBenefit(ctx, &walletv1.CheckVipBenefitRequest{ + RequestId: requestID, + AppCode: appcode.FromContext(ctx), + UserId: userID, + BenefitCode: benefitCode, + }) + if err != nil { + return false, err + } + return resp.GetAllowed(), nil +} + // NewGRPC 基于共享 gRPC 连接创建 wallet-service 资源读取客户端。 func NewGRPC(conn grpc.ClientConnInterface) *Client { return &Client{client: walletv1.NewWalletServiceClient(conn)} diff --git a/services/user-service/internal/service/user/service.go b/services/user-service/internal/service/user/service.go index b463527d..e27e4554 100644 --- a/services/user-service/internal/service/user/service.go +++ b/services/user-service/internal/service/user/service.go @@ -73,6 +73,20 @@ type UserRepository interface { ChangeUserCountry(ctx context.Context, command userdomain.CountryChangeCommand) (userdomain.User, int64, []string, error) } +// AnonymousProfileVisitRepository 是匿名访问的可选持久化能力。 +// 它与旧的 UserRepository 分离,避免把新增隐私表方法扩散到只覆盖基础用户接口的测试替身; +// 生产 MySQL repository 必须实现该接口,匿名写入缺失时 service 会拒绝降级为公开访问。 +type AnonymousProfileVisitRepository interface { + // RecordAnonymousProfileVisit 把本次访问固化为匿名事实;同一 target/visitor 只累计同一条匿名记录。 + RecordAnonymousProfileVisit(ctx context.Context, visitorUserID int64, targetUserID int64, anonymousVisitID string, nowMs int64) (bool, userdomain.ProfileStats, error) +} + +// PrivacyBenefitChecker 只暴露 user-service 执行资料隐私所需的单项 VIP 权益判断。 +// 具体权益、体验卡排除项和用户开关均由 wallet-service 统一计算,user-service 不复制 VIP 规则。 +type PrivacyBenefitChecker interface { + CheckVipBenefit(ctx context.Context, requestID string, userID int64, benefitCode string) (bool, error) +} + // AppRegistryRepository 负责把客户端包名解析成内部 app_code。 type AppRegistryRepository interface { ResolveApp(ctx context.Context, appCode string, packageName string, platform string) (userdomain.App, error) @@ -220,6 +234,10 @@ type Service struct { appRegistryRepository AppRegistryRepository // userRepository 持有 users 主表视角的读写能力。 userRepository UserRepository + // anonymousProfileVisitRepository 把匿名足迹保存在独立 owner 表,避免改写历史公开访问表主键。 + anonymousProfileVisitRepository AnonymousProfileVisitRepository + // privacyBenefitChecker 实时读取 wallet-service 计算后的隐私权益;RPC 失败时业务层按保护隐私处理。 + privacyBenefitChecker PrivacyBenefitChecker // identityRepository 持有短号、靓号和变更日志事务能力。 identityRepository IdentityRepository // levelProfileReader 读取 activity-service 等级投影,靓号池申请必须依赖它判断等级区间。 @@ -277,6 +295,10 @@ func New(userRepository UserRepository, options ...Option) *Service { // MySQL 用户 repository 同时持有 auth_sessions 表的同库事务能力。 svc.moderationRepository = repository } + if repository, ok := userRepository.(AnonymousProfileVisitRepository); ok { + // MySQL 用户 repository 同时持有公开和匿名足迹表,写入路径在同一 owner 数据库内保持原子。 + svc.anonymousProfileVisitRepository = repository + } for _, option := range options { // option 只改依赖或策略,不做 IO。 option(svc) @@ -285,6 +307,15 @@ func New(userRepository UserRepository, options ...Option) *Service { return svc } +// WithPrivacyBenefitChecker 注入 wallet-service 的最终 VIP 权益判断。 +func WithPrivacyBenefitChecker(checker PrivacyBenefitChecker) Option { + return func(s *Service) { + if checker != nil { + s.privacyBenefitChecker = checker + } + } +} + // WithModerationRepository 注入用户治理事务 repository。 func WithModerationRepository(repository ModerationRepository) Option { return func(s *Service) { diff --git a/services/user-service/internal/service/user/social.go b/services/user-service/internal/service/user/social.go index d1e629e3..27dd7523 100644 --- a/services/user-service/internal/service/user/social.go +++ b/services/user-service/internal/service/user/social.go @@ -2,6 +2,8 @@ package user import ( "context" + "crypto/rand" + "encoding/hex" "strings" "hyapp/pkg/xerr" @@ -11,20 +13,105 @@ import ( const ( defaultSocialPageSize = 20 maxSocialPageSize = 100 + + // 这两个 code 必须与 wallet-service 的可配置 VIP benefit code 保持一致; + // user-service 只消费最终 allowed 结果,不复制等级、体验卡和用户开关规则。 + hideProfileDataBenefitCode = "hide_profile_data" + anonymousProfileVisitBenefitCode = "anonymous_profile_visit" ) +// ProfileVisitResult 是一次资料访问的隐私收敛结果。 +// TargetStatsHidden=true 时 TargetStats 必须保持零值,transport 也必须省略该对象,形成双层防泄漏。 +type ProfileVisitResult struct { + Recorded bool + TargetStats userdomain.ProfileStats + TargetStatsHidden bool +} + // RecordProfileVisit 记录访问关系;自己访问自己不进入访问计数。 -func (s *Service) RecordProfileVisit(ctx context.Context, visitorUserID int64, targetUserID int64) (bool, userdomain.ProfileStats, error) { +func (s *Service) RecordProfileVisit(ctx context.Context, requestID string, visitorUserID int64, targetUserID int64) (ProfileVisitResult, error) { if visitorUserID <= 0 || targetUserID <= 0 { - return false, userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "user_id is required") + return ProfileVisitResult{}, xerr.New(xerr.InvalidArgument, "user_id is required") } if visitorUserID == targetUserID { - return false, userdomain.ProfileStats{}, xerr.New(xerr.InvalidArgument, "cannot visit self") + return ProfileVisitResult{}, xerr.New(xerr.InvalidArgument, "cannot visit self") } if s.userRepository == nil { - return false, userdomain.ProfileStats{}, xerr.New(xerr.Unavailable, "user repository is not configured") + return ProfileVisitResult{}, xerr.New(xerr.Unavailable, "user repository is not configured") } - return s.userRepository.RecordProfileVisit(ctx, visitorUserID, targetUserID, s.now().UnixMilli()) + + // 两项隐私都必须在访问发生时实时判断:目标用户的数据隐藏影响本次响应,访客匿名则决定 + // 本次事实写入哪张表。目标权益查询失败时仅隐藏 stats;匿名权益查询失败时中止写入,既不泄漏身份,也不越权授予付费匿名能力。 + anonymousVisit, err := s.anonymousVisitAllowed(ctx, requestID, visitorUserID) + if err != nil { + return ProfileVisitResult{}, err + } + // 匿名门禁先成功,才读取目标用户的当前隐藏设置;这样 wallet 整体不可用时不做无意义第二次 RPC, + // 正常链路下 target_stats 的判定也更接近最终响应时点。 + targetStatsHidden := s.targetStatsMustBeHidden(ctx, requestID, targetUserID) + nowMs := s.now().UnixMilli() + + var ( + recorded bool + stats userdomain.ProfileStats + writeErr error + ) + if anonymousVisit { + if s.anonymousProfileVisitRepository == nil { + // 匿名 owner 表缺失时必须拒绝写入,绝不能悄悄回退到公开足迹表暴露真实 visitor_user_id。 + return ProfileVisitResult{}, xerr.New(xerr.Unavailable, "anonymous profile visit repository is not configured") + } + anonymousVisitID, generateErr := newAnonymousVisitID() + if generateErr != nil { + return ProfileVisitResult{}, xerr.New(xerr.Unavailable, "anonymous visit id generation failed") + } + recorded, stats, writeErr = s.anonymousProfileVisitRepository.RecordAnonymousProfileVisit(ctx, visitorUserID, targetUserID, anonymousVisitID, nowMs) + } else { + // 当前不具备匿名权益时仍写旧公开表;已有匿名历史不会被迁移或反向暴露。 + recorded, stats, writeErr = s.userRepository.RecordProfileVisit(ctx, visitorUserID, targetUserID, nowMs) + } + if writeErr != nil { + return ProfileVisitResult{}, writeErr + } + + result := ProfileVisitResult{Recorded: recorded, TargetStatsHidden: targetStatsHidden} + if !targetStatsHidden { + result.TargetStats = stats + } + return result, nil +} + +// targetStatsMustBeHidden 收敛目标资料统计的可见性。 +// checker 缺失或 RPC 失败都隐藏 stats:这是只读保护,不会因此授予目标用户任何可执行付费能力。 +func (s *Service) targetStatsMustBeHidden(ctx context.Context, requestID string, userID int64) bool { + if s.privacyBenefitChecker == nil { + return true + } + allowed, err := s.privacyBenefitChecker.CheckVipBenefit(ctx, requestID, userID, hideProfileDataBenefitCode) + return err != nil || allowed +} + +// anonymousVisitAllowed 判断本次足迹能否匿名。 +// 依赖失败时不能回退公开写入,也不能把失败当 allowed 授予付费权益,因此明确返回 UNAVAILABLE 且不落任何访问事实。 +func (s *Service) anonymousVisitAllowed(ctx context.Context, requestID string, userID int64) (bool, error) { + if s.privacyBenefitChecker == nil { + return false, xerr.New(xerr.Unavailable, "privacy benefit checker is not configured") + } + allowed, err := s.privacyBenefitChecker.CheckVipBenefit(ctx, requestID, userID, anonymousProfileVisitBenefitCode) + if err != nil { + return false, xerr.New(xerr.Unavailable, "anonymous profile visit benefit is unavailable") + } + return allowed, nil +} + +// newAnonymousVisitID 生成不含 user_id、时间或 target 信息的随机展示 ID。 +// ID 在首次插入后由数据库保持不变;同一访客访问不同 target 会生成不同 ID,客户端无法跨用户关联。 +func newAnonymousVisitID() (string, error) { + var entropy [16]byte + if _, err := rand.Read(entropy[:]); err != nil { + return "", err + } + return "anon_" + hex.EncodeToString(entropy[:]), nil } func (s *Service) ListProfileVisitors(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.ProfileVisitRecord, int64, error) { diff --git a/services/user-service/internal/storage/mysql/user/social_repository.go b/services/user-service/internal/storage/mysql/user/social_repository.go index e03f53c6..38e186c9 100644 --- a/services/user-service/internal/storage/mysql/user/social_repository.go +++ b/services/user-service/internal/storage/mysql/user/social_repository.go @@ -3,13 +3,21 @@ package user import ( "context" "database/sql" + "errors" "strings" + mysqldriver "github.com/go-sql-driver/mysql" "hyapp/pkg/appcode" "hyapp/pkg/xerr" userdomain "hyapp/services/user-service/internal/domain/user" ) +const ( + profileVisitorPageMax int32 = 100 + profileVisitorPageSizeMax int32 = 100 + profileVisitorReadWindowMax int64 = 10_000 +) + // RecordProfileVisit 写访问去重记录。重复访问只刷新时间和次数,不重复增加 visitors_count。 func (r *Repository) RecordProfileVisit(ctx context.Context, visitorUserID int64, targetUserID int64, nowMs int64) (bool, userdomain.ProfileStats, error) { tx, err := r.db.BeginTx(ctx, nil) @@ -61,37 +69,222 @@ func (r *Repository) RecordProfileVisit(ctx context.Context, visitorUserID int64 return inserted, stats, tx.Commit() } -func (r *Repository) ListProfileVisitors(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.ProfileVisitRecord, int64, error) { +// RecordAnonymousProfileVisit 把匿名足迹固化到独立表。 +// 真实 visitor_user_id 只作为 owner 内部聚合键;对外随机 ID 首次写入后永不覆盖,因此 VIP 到期也不会反向暴露历史身份。 +func (r *Repository) RecordAnonymousProfileVisit(ctx context.Context, visitorUserID int64, targetUserID int64, anonymousVisitID string, nowMs int64) (bool, userdomain.ProfileStats, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return false, userdomain.ProfileStats{}, err + } + defer tx.Rollback() + appCode := appcode.FromContext(ctx) - total, err := countRows(ctx, r.db, ` - SELECT COUNT(*) FROM user_profile_visits WHERE app_code = ? AND target_user_id = ?`, - appCode, userID, + if err := ensureSocialUser(ctx, tx, appCode, visitorUserID); err != nil { + return false, userdomain.ProfileStats{}, err + } + if err := ensureSocialUser(ctx, tx, appCode, targetUserID); err != nil { + return false, userdomain.ProfileStats{}, err + } + if err := ensureProfileStatsRow(ctx, tx, appCode, targetUserID, nowMs); err != nil { + return false, userdomain.ProfileStats{}, err + } + _, err = tx.ExecContext(ctx, ` + INSERT INTO user_profile_anonymous_visits ( + app_code, target_user_id, visitor_user_id, anonymous_visit_id, visit_count, + first_visited_at_ms, last_visited_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, 1, ?, ?, ?)`, + appCode, targetUserID, visitorUserID, anonymousVisitID, nowMs, nowMs, nowMs, ) + inserted := err == nil + if err != nil { + if !anonymousVisitDuplicate(err) { + return false, userdomain.ProfileStats{}, err + } + // 主键冲突表示同一 target/visitor 的历史匿名记录,按主键累计且保留首次随机 ID。 + // 若冲突仅来自随机 ID 唯一键,则本 visitor 主键不存在、UPDATE affected=0,事务会 fail-closed 回滚。 + result, updateErr := tx.ExecContext(ctx, ` + UPDATE user_profile_anonymous_visits + SET visit_count = visit_count + 1, last_visited_at_ms = ?, updated_at_ms = ? + WHERE app_code = ? AND target_user_id = ? AND visitor_user_id = ?`, + nowMs, nowMs, appCode, targetUserID, visitorUserID, + ) + if updateErr != nil { + return false, userdomain.ProfileStats{}, updateErr + } + affected, affectedErr := result.RowsAffected() + if affectedErr != nil { + return false, userdomain.ProfileStats{}, affectedErr + } + if affected != 1 { + return false, userdomain.ProfileStats{}, xerr.New(xerr.Conflict, "anonymous visit id collision") + } + } + if inserted { + // 公开与匿名记录是两个独立展示聚合;同一真实用户先公开后匿名会各占一个访客项, + // 这样历史公开事实不会被改写,匿名阶段也不会借旧记录泄漏身份。 + if _, err := tx.ExecContext(ctx, ` + UPDATE user_profile_stats + SET visitors_count = visitors_count + 1, updated_at_ms = ? + WHERE app_code = ? AND user_id = ?`, + nowMs, appCode, targetUserID, + ); err != nil { + return false, userdomain.ProfileStats{}, err + } + } + stats, err := queryProfileStatsTx(ctx, tx, appCode, targetUserID) + if err != nil { + return false, userdomain.ProfileStats{}, err + } + return inserted, stats, tx.Commit() +} + +func (r *Repository) ListProfileVisitors(ctx context.Context, userID int64, page int32, pageSize int32) ([]userdomain.ProfileVisitRecord, int64, error) { + // gRPC 内部调用也必须受同一有界窗口保护,不能只相信 gateway 的 HTTP 参数校验。 + // page/page_size 双上限固定公开协议;window 上限再防御未来任一上限被单独调整后重新引入无界物化。 + if page <= 0 || page > profileVisitorPageMax || pageSize <= 0 || pageSize > profileVisitorPageSizeMax { + return nil, 0, xerr.New(xerr.InvalidArgument, "profile visitor page is out of range") + } + offset := socialOffset(page, pageSize) + if offset+int64(pageSize) > profileVisitorReadWindowMax { + return nil, 0, xerr.New(xerr.InvalidArgument, "profile visitor page window is too large") + } + appCode := appcode.FromContext(ctx) + // total 与两张表的分页读取放在同一个只读快照中,避免并发访问恰好落在查询之间时出现 + // total 和 records 不属于同一时点。COUNT 都命中 (app_code,target_user_id,...) 前缀,不扫全表。 + tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}) if err != nil { return nil, 0, err } - rows, err := r.db.QueryContext(ctx, ` + defer tx.Rollback() + var total int64 + if err := tx.QueryRowContext(ctx, ` + SELECT + (SELECT COUNT(*) FROM user_profile_visits WHERE app_code = ? AND target_user_id = ?) + + (SELECT COUNT(*) FROM user_profile_anonymous_visits WHERE app_code = ? AND target_user_id = ?)`, + appCode, userID, appCode, userID, + ).Scan(&total); err != nil { + return nil, 0, err + } + if total == 0 { + return []userdomain.ProfileVisitRecord{}, 0, tx.Commit() + } + + if offset >= total { + // 越过末页时直接返回快照 total,避免为了一个确定为空的页面从两个分支各读取全部目标记录。 + return []userdomain.ProfileVisitRecord{}, total, tx.Commit() + } + fetchLimit := offset + int64(pageSize) + if fetchLimit > total { + fetchLimit = total + } + publicRecords, err := queryPublicProfileVisitors(ctx, tx, appCode, userID, fetchLimit) + if err != nil { + return nil, 0, err + } + anonymousRecords, err := queryAnonymousProfileVisitors(ctx, tx, appCode, userID, fetchLimit) + if err != nil { + return nil, 0, err + } + records := mergeProfileVisitors(publicRecords, anonymousRecords, offset, pageSize) + if err := tx.Commit(); err != nil { + return nil, 0, err + } + return records, total, nil +} + +func queryPublicProfileVisitors(ctx context.Context, tx *sql.Tx, appCode string, userID int64, limit int64) ([]userdomain.ProfileVisitRecord, error) { + rows, err := tx.QueryContext(ctx, ` SELECT visitor_user_id, target_user_id, visit_count, last_visited_at_ms FROM user_profile_visits WHERE app_code = ? AND target_user_id = ? ORDER BY last_visited_at_ms DESC, visitor_user_id DESC - LIMIT ? OFFSET ?`, - appCode, userID, pageSize, socialOffset(page, pageSize), + LIMIT ?`, + appCode, userID, limit, ) if err != nil { - return nil, 0, err + return nil, err } defer rows.Close() - records := make([]userdomain.ProfileVisitRecord, 0, pageSize) + records := make([]userdomain.ProfileVisitRecord, 0) for rows.Next() { var record userdomain.ProfileVisitRecord if err := rows.Scan(&record.VisitorUserID, &record.TargetUserID, &record.VisitCount, &record.LastVisitedAtMs); err != nil { - return nil, 0, err + return nil, err } records = append(records, record) } - return records, total, rows.Err() + return records, rows.Err() +} + +func queryAnonymousProfileVisitors(ctx context.Context, tx *sql.Tx, appCode string, userID int64, limit int64) ([]userdomain.ProfileVisitRecord, error) { + rows, err := tx.QueryContext(ctx, ` + SELECT anonymous_visit_id, target_user_id, visit_count, last_visited_at_ms + FROM user_profile_anonymous_visits + WHERE app_code = ? AND target_user_id = ? + ORDER BY last_visited_at_ms DESC, anonymous_visit_id DESC + LIMIT ?`, + appCode, userID, limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + records := make([]userdomain.ProfileVisitRecord, 0) + for rows.Next() { + record := userdomain.ProfileVisitRecord{VisitorIdentityHidden: true} + if err := rows.Scan(&record.AnonymousVisitID, &record.TargetUserID, &record.VisitCount, &record.LastVisitedAtMs); err != nil { + return nil, err + } + records = append(records, record) + } + return records, rows.Err() +} + +// mergeProfileVisitors 合并两个已排序分支,并在内存中应用全局 offset。 +// 每个 SQL 分支最多读取 offset+page_size 行,避免 UNION ALL 为单个高访问目标物化其全部历史记录。 +func mergeProfileVisitors(publicRecords []userdomain.ProfileVisitRecord, anonymousRecords []userdomain.ProfileVisitRecord, offset int64, pageSize int32) []userdomain.ProfileVisitRecord { + records := make([]userdomain.ProfileVisitRecord, 0, pageSize) + publicIndex, anonymousIndex := 0, 0 + var skipped int64 + for (publicIndex < len(publicRecords) || anonymousIndex < len(anonymousRecords)) && len(records) < int(pageSize) { + var next userdomain.ProfileVisitRecord + switch { + case anonymousIndex >= len(anonymousRecords): + next = publicRecords[publicIndex] + publicIndex++ + case publicIndex >= len(publicRecords): + next = anonymousRecords[anonymousIndex] + anonymousIndex++ + case profileVisitBefore(publicRecords[publicIndex], anonymousRecords[anonymousIndex]): + next = publicRecords[publicIndex] + publicIndex++ + default: + next = anonymousRecords[anonymousIndex] + anonymousIndex++ + } + if skipped < offset { + skipped++ + continue + } + records = append(records, next) + } + return records +} + +func profileVisitBefore(left userdomain.ProfileVisitRecord, right userdomain.ProfileVisitRecord) bool { + if left.LastVisitedAtMs != right.LastVisitedAtMs { + return left.LastVisitedAtMs > right.LastVisitedAtMs + } + if left.VisitorIdentityHidden != right.VisitorIdentityHidden { + // 同毫秒内公开记录固定排在匿名记录前,避免不同查询计划造成跨页漂移。 + return !left.VisitorIdentityHidden + } + if left.VisitorIdentityHidden { + return left.AnonymousVisitID > right.AnonymousVisitID + } + return left.VisitorUserID > right.VisitorUserID } func (r *Repository) FollowUser(ctx context.Context, followerUserID int64, followeeUserID int64, nowMs int64) (userdomain.ProfileStats, error) { @@ -601,6 +794,12 @@ func countRows(ctx context.Context, db *sql.DB, query string, args ...any) (int6 return total, err } -func socialOffset(page int32, pageSize int32) int32 { - return (page - 1) * pageSize +func socialOffset(page int32, pageSize int32) int64 { + // 转为 int64 后再相乘,避免合法 int32 page 在高页码时先于 SQL LIMIT/OFFSET 发生整数溢出。 + return (int64(page) - 1) * int64(pageSize) +} + +func anonymousVisitDuplicate(err error) bool { + var mysqlErr *mysqldriver.MySQLError + return errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 } diff --git a/services/user-service/internal/transport/grpc/convert.go b/services/user-service/internal/transport/grpc/convert.go index a78e9df6..ec5b1a67 100644 --- a/services/user-service/internal/transport/grpc/convert.go +++ b/services/user-service/internal/transport/grpc/convert.go @@ -246,10 +246,12 @@ func toProtoProfileStats(stats userdomain.ProfileStats) *userv1.UserProfileStats func toProtoProfileVisitRecord(record userdomain.ProfileVisitRecord) *userv1.ProfileVisitRecord { return &userv1.ProfileVisitRecord{ - VisitorUserId: record.VisitorUserID, - TargetUserId: record.TargetUserID, - VisitCount: record.VisitCount, - LastVisitedAtMs: record.LastVisitedAtMs, + VisitorUserId: record.VisitorUserID, + TargetUserId: record.TargetUserID, + VisitCount: record.VisitCount, + LastVisitedAtMs: record.LastVisitedAtMs, + VisitorIdentityHidden: record.VisitorIdentityHidden, + AnonymousVisitId: record.AnonymousVisitID, } } diff --git a/services/user-service/internal/transport/grpc/server.go b/services/user-service/internal/transport/grpc/server.go index 22fa14c4..67d2f993 100644 --- a/services/user-service/internal/transport/grpc/server.go +++ b/services/user-service/internal/transport/grpc/server.go @@ -596,11 +596,19 @@ func toProtoSetUserStatusResponse(result userservice.UserStatusResult) *userv1.S // RecordProfileVisit 记录访问足迹;重复访问只刷新访问次数和最后访问时间。 func (s *Server) RecordProfileVisit(ctx context.Context, req *userv1.RecordProfileVisitRequest) (*userv1.RecordProfileVisitResponse, error) { ctx = contextWithApp(ctx, req.GetMeta()) - recorded, stats, err := s.userSvc.RecordProfileVisit(ctx, req.GetVisitorUserId(), req.GetTargetUserId()) + result, err := s.userSvc.RecordProfileVisit(ctx, requestIDFromMeta(req.GetMeta()), req.GetVisitorUserId(), req.GetTargetUserId()) if err != nil { return nil, xerr.ToGRPCError(err) } - return &userv1.RecordProfileVisitResponse{Recorded: recorded, TargetStats: toProtoProfileStats(stats)}, nil + resp := &userv1.RecordProfileVisitResponse{ + Recorded: result.Recorded, + TargetStatsHidden: result.TargetStatsHidden, + } + if !result.TargetStatsHidden { + // service 已在隐藏分支清空 stats;transport 再按显式标志省略 protobuf 对象,避免零值对象被客户端误认为真实数据。 + resp.TargetStats = toProtoProfileStats(result.TargetStats) + } + return resp, nil } // ListProfileVisitors 返回访问当前用户主页的人,按最后访问时间倒序。 diff --git a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql index 06caa9f6..44088acf 100644 --- a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql +++ b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql @@ -1457,6 +1457,23 @@ CREATE TABLE IF NOT EXISTS user_resource_equipment ( KEY idx_user_resource_equipment_user (app_code, user_id, updated_at_ms) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备表'; +-- 装备命令以 (app_code,command_id) 点查并先于 entitlement/equipment 行加锁;不对装备表做历史扫描。 +-- 记录首个成功回包快照,确保网络重试不会因资源随后到期或后台下架而改变业务结果。 +CREATE TABLE IF NOT EXISTS user_resource_equipment_commands ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + command_id VARCHAR(128) NOT NULL COMMENT '装备业务幂等 ID', + request_hash CHAR(64) NOT NULL COMMENT '用户、资源和指定 entitlement 的请求语义哈希', + user_id BIGINT NOT NULL COMMENT '操作用户 ID', + resource_id BIGINT NOT NULL COMMENT '目标资源 ID', + requested_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '客户端指定的 entitlement;空表示服务端选择', + result_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首个成功命中的 entitlement', + resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '首个成功结果的资源类型', + response_json JSON NULL COMMENT '首个成功 UserResourceEntitlement 回包快照', + created_at_ms BIGINT NOT NULL COMMENT '首次命令时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '结果完成时间,UTC epoch ms', + PRIMARY KEY (app_code, command_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备命令幂等事实表'; + CREATE TABLE IF NOT EXISTS wallet_diamond_exchange_rules ( app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离', rule_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '规则 ID', @@ -1913,9 +1930,31 @@ CREATE TABLE IF NOT EXISTS vip_level_benefits ( updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', PRIMARY KEY (app_code, level, benefit_code), KEY idx_vip_benefit_code (app_code, benefit_code, status, level), - KEY idx_vip_benefit_level_sort (app_code, level, status, sort_order) + KEY idx_vip_benefit_level_sort (app_code, level, status, sort_order), + KEY idx_vip_benefit_resource (app_code, resource_id, status, benefit_code, level) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 每等级完整权益矩阵'; +-- admin 每次更新都会在删除当前矩阵前后各写一份不可变快照。运行时只按资源索引读取这张 +-- 小型配置历史,从而让改绑/删除/program disabled 后的旧 entitlement 继续失败关闭。 +CREATE TABLE IF NOT EXISTS vip_benefit_resource_binding_history ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + history_id CHAR(64) NOT NULL COMMENT '配置版本、阶段、等级和权益的稳定哈希', + config_version BIGINT NOT NULL COMMENT '写入该快照的 VIP 配置版本', + change_phase VARCHAR(16) NOT NULL COMMENT 'baseline/before/after', + program_type VARCHAR(32) NOT NULL COMMENT '快照时的 VIP program 类型', + level INT NOT NULL COMMENT 'VIP 等级', + benefit_code VARCHAR(64) NOT NULL COMMENT '权益编码', + benefit_status VARCHAR(32) NOT NULL COMMENT '权益快照状态', + trial_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '体验卡是否可用快照', + resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '当时绑定资源 ID;0 表示未绑定', + resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '当时要求的资源类型', + auto_equip BOOLEAN NOT NULL DEFAULT FALSE COMMENT '当时自动装备策略', + recorded_at_ms BIGINT NOT NULL COMMENT '记录时间,UTC epoch ms', + PRIMARY KEY (app_code, history_id), + UNIQUE KEY uk_vip_binding_history_snapshot (app_code, config_version, change_phase, level, benefit_code), + KEY idx_vip_binding_history_resource (app_code, resource_id, program_type, benefit_code, level) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 权益资源绑定不可变历史'; + -- user_vip_trial_cards 只保存体验卡的 VIP 元数据;卡片库存与当前佩戴仍分别复用 -- user_resource_entitlements 和 user_resource_equipment,切换佩戴不会重置或作废其它卡片。 CREATE TABLE IF NOT EXISTS user_vip_trial_cards ( @@ -2067,7 +2106,14 @@ SELECT 'fami', levels.level, benefits.benefit_code, benefits.name, benefits.benefit_type, benefits.unlock_level, CASE WHEN benefits.benefit_code = 'daily_coin_rebate' THEN 'disabled' ELSE 'active' END, - benefits.trial_enabled, 0, '', benefits.execution_scope, + benefits.trial_enabled, 0, + CASE benefits.benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + ELSE '' + END, + benefits.execution_scope, benefits.auto_equip, benefits.sort_order, CASE WHEN benefits.benefit_code = 'online_global_notice' @@ -2097,7 +2143,7 @@ INNER JOIN ( UNION ALL SELECT 'mic_skin', '麦克风皮肤', 'decoration', 4, TRUE, 'room', TRUE, 150 UNION ALL SELECT 'room_border', '房间边框', 'decoration', 4, TRUE, 'room', TRUE, 160 UNION ALL SELECT 'vehicle', 'VIP座驾', 'decoration', 4, TRUE, 'room', TRUE, 170 - UNION ALL SELECT 'colored_room_name', 'VIP彩色房间名称', 'function', 5, TRUE, 'room', FALSE, 180 + UNION ALL SELECT 'colored_room_name', 'VIP彩色房间名称', 'decoration', 5, TRUE, 'room', FALSE, 180 UNION ALL SELECT 'colored_nickname', 'VIP彩色昵称', 'function', 5, TRUE, 'user', FALSE, 190 UNION ALL SELECT 'colored_id', 'VIP彩色ID', 'function', 5, TRUE, 'user', FALSE, 200 UNION ALL SELECT 'gift_tray_skin', '送礼托盘皮肤', 'function', 5, TRUE, 'room', FALSE, 210 @@ -2164,6 +2210,168 @@ INSERT IGNORE INTO resources ( ('fami', 'vip_trial_card_9', 'vip_trial_card', 'VIP9体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":9}', 90, 0, 0, 0, 0); +SET @vip_resource_contract_now_ms := CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED); + +-- 三项房间视觉权益的 code -> resource_type 是跨服务稳定契约。老环境先收敛类型; +-- disabled 行可以没有素材,但不能保留指向下架资源或错误类型资源的脏 ID。 +UPDATE vip_level_benefits +SET benefit_type = 'decoration', + resource_type = CASE benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + END, + updated_at_ms = @vip_resource_contract_now_ms +WHERE app_code = 'fami' + AND benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') + AND ( + benefit_type <> 'decoration' + OR resource_type <> CASE benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + END + ); + +UPDATE vip_level_benefits AS benefit +LEFT JOIN resources AS resource + ON resource.app_code = benefit.app_code + AND resource.resource_id = benefit.resource_id + AND resource.status = 'active' + AND resource.resource_type = CASE benefit.benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + END +SET benefit.resource_id = 0, + benefit.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') + AND benefit.resource_id > 0 + AND resource.resource_id IS NULL; + +-- 只按已经存在的 active Fami VIP4..VIP8 资源组唯一匹配麦位动画;HAVING=1 防止多候选误绑。 +-- VIP9、room_border、room_name_style 没有可核实目录资源时继续保持 resource_id=0,不生成占位素材。 +UPDATE vip_level_benefits AS benefit +JOIN ( + SELECT resource_group.group_code, MIN(resource.resource_id) AS resource_id + FROM resource_groups AS resource_group + JOIN resource_group_items AS group_item + ON group_item.app_code = resource_group.app_code + AND group_item.group_id = resource_group.group_id + AND group_item.item_type = 'resource' + JOIN resources AS resource + ON resource.app_code = group_item.app_code + AND resource.resource_id = group_item.resource_id + AND resource.resource_type = 'mic_seat_animation' + AND resource.status = 'active' + WHERE resource_group.app_code = 'fami' + AND resource_group.status = 'active' + AND resource_group.group_code IN ('VIP4', 'VIP5', 'VIP6', 'VIP7', 'VIP8') + GROUP BY resource_group.group_code + HAVING COUNT(DISTINCT resource.resource_id) = 1 +) AS mapping + ON mapping.group_code = CONCAT('VIP', benefit.level) +SET benefit.resource_id = mapping.resource_id, + benefit.resource_type = 'mic_seat_animation', + benefit.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.level BETWEEN 4 AND 8 + AND benefit.benefit_code = 'mic_skin' + AND benefit.status = 'active' + AND benefit.resource_id = 0; + +-- 只给 active mic_skin 已绑定的真实麦座动画补 format;优先采用合法 animation_format, +-- 否则只从实际 animation/asset URL 的明确后缀推导,jpg 统一写成 jpeg。 +UPDATE vip_level_benefits AS benefit +JOIN resources AS resource FORCE INDEX (PRIMARY) + ON resource.resource_id = benefit.resource_id + AND resource.app_code = benefit.app_code + AND resource.resource_type = 'mic_seat_animation' + AND resource.status = 'active' +SET resource.metadata_json = JSON_SET( + COALESCE(resource.metadata_json, JSON_OBJECT()), + '$.format', + CASE + WHEN LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) + IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + THEN LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')))) + WHEN LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1)) + REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' + THEN CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1)) + WHEN 'jpg' THEN 'jpeg' + ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1)) + END + ELSE CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1)) + WHEN 'jpg' THEN 'jpeg' + ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1)) + END + END + ), + resource.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.benefit_code = 'mic_skin' + AND benefit.status = 'active' + AND benefit.resource_type = 'mic_seat_animation' + AND benefit.resource_id > 0 + AND (TRIM(resource.asset_url) <> '' OR TRIM(resource.animation_url) <> '') + AND TRIM(resource.preview_url) <> '' + AND LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), ''))) + NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + AND ( + LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) + IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1)) + REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' + OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1)) + REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' + ); + +-- active 装扮必须能联结到同 App、active、精确类型的真实资源。麦座动画和房间边框还 +-- 必须具备素材、preview、合法 format,并保持可选 animation_format 与 format 一致。 +UPDATE vip_level_benefits AS benefit +LEFT JOIN resources AS resource + ON resource.app_code = benefit.app_code + AND resource.resource_id = benefit.resource_id + AND resource.status = 'active' + AND resource.resource_type = benefit.resource_type +SET benefit.status = 'disabled', + benefit.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') + AND benefit.status = 'active' + AND ( + resource.resource_id IS NULL + OR ( + benefit.resource_type IN ('mic_seat_animation', 'room_border') + AND ( + (TRIM(resource.asset_url) = '' AND TRIM(resource.animation_url) = '') + OR TRIM(resource.preview_url) = '' + OR LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), ''))) + NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + OR ( + LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) <> '' + AND LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')))) + <> LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')))) + ) + ) + ) + ); + +-- fresh init 在资源映射收敛后建立 baseline;重放 initdb 由唯一键保持幂等。 +INSERT IGNORE INTO vip_benefit_resource_binding_history ( + app_code, history_id, config_version, change_phase, program_type, level, + benefit_code, benefit_status, trial_enabled, resource_id, resource_type, + auto_equip, recorded_at_ms +) +SELECT b.app_code, + SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256), + p.config_version, 'baseline', p.program_type, b.level, b.benefit_code, + b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip, + @vip_resource_contract_now_ms +FROM vip_level_benefits AS b +JOIN vip_program_configs AS p ON p.app_code = b.app_code; + INSERT IGNORE INTO gift_type_configs ( app_code, type_code, name, tab_key, status, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms diff --git a/services/wallet-service/deploy/mysql/migrations/003_vip_resource_equipment_contract.sql b/services/wallet-service/deploy/mysql/migrations/003_vip_resource_equipment_contract.sql new file mode 100644 index 00000000..66b3c669 --- /dev/null +++ b/services/wallet-service/deploy/mysql/migrations/003_vip_resource_equipment_contract.sql @@ -0,0 +1,247 @@ +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_wallet; + +-- 与顶层生产迁移 069 保持同一事实;本服务迁移供本地/独立 wallet 部署使用。 +-- 幂等重放只走 (app_code,command_id) 主键,不扫描装备或 entitlement 业务表。 +CREATE TABLE IF NOT EXISTS user_resource_equipment_commands ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + command_id VARCHAR(128) NOT NULL COMMENT '装备业务幂等 ID', + request_hash CHAR(64) NOT NULL COMMENT '用户、资源和指定 entitlement 的请求语义哈希', + user_id BIGINT NOT NULL COMMENT '操作用户 ID', + resource_id BIGINT NOT NULL COMMENT '目标资源 ID', + requested_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '客户端指定的 entitlement;空表示服务端选择', + result_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首个成功命中的 entitlement', + resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '首个成功结果的资源类型', + response_json JSON NULL COMMENT '首个成功 UserResourceEntitlement 回包快照', + created_at_ms BIGINT NOT NULL COMMENT '首次命令时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '结果完成时间,UTC epoch ms', + PRIMARY KEY (app_code, command_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备命令幂等事实表'; + +-- 配置矩阵很小,历史表按 admin config_version 追加;装备和 appearance 只按资源覆盖索引点查。 +-- 记录 program_type,避免 legacy_timed App 的普通资源被 tiered VIP 门禁误伤。 +CREATE TABLE IF NOT EXISTS vip_benefit_resource_binding_history ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + history_id CHAR(64) NOT NULL COMMENT '配置版本、阶段、等级和权益的稳定哈希', + config_version BIGINT NOT NULL COMMENT '写入该快照的 VIP 配置版本', + change_phase VARCHAR(16) NOT NULL COMMENT 'baseline/before/after', + program_type VARCHAR(32) NOT NULL COMMENT '快照时的 VIP program 类型', + level INT NOT NULL COMMENT 'VIP 等级', + benefit_code VARCHAR(64) NOT NULL COMMENT '权益编码', + benefit_status VARCHAR(32) NOT NULL COMMENT '权益快照状态', + trial_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '体验卡是否可用快照', + resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '当时绑定资源 ID;0 表示未绑定', + resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '当时要求的资源类型', + auto_equip BOOLEAN NOT NULL DEFAULT FALSE COMMENT '当时自动装备策略', + recorded_at_ms BIGINT NOT NULL COMMENT '记录时间,UTC epoch ms', + PRIMARY KEY (app_code, history_id), + UNIQUE KEY uk_vip_binding_history_snapshot (app_code, config_version, change_phase, level, benefit_code), + KEY idx_vip_binding_history_resource (app_code, resource_id, program_type, benefit_code, level) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 权益资源绑定不可变历史'; + +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_level_benefits' + AND INDEX_NAME = 'idx_vip_benefit_resource') = 0, + 'ALTER TABLE vip_level_benefits ADD INDEX idx_vip_benefit_resource (app_code, resource_id, status, benefit_code, level), ALGORITHM=INPLACE, LOCK=NONE', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +SET @vip_resource_contract_now_ms := CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED); + +-- 在任何修正规则执行前保存旧映射;后续 admin 更新还会为每个新版本分别追加 before/after。 +INSERT IGNORE INTO vip_benefit_resource_binding_history ( + app_code, history_id, config_version, change_phase, program_type, level, + benefit_code, benefit_status, trial_enabled, resource_id, resource_type, + auto_equip, recorded_at_ms +) +SELECT b.app_code, + SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256), + p.config_version, 'baseline', p.program_type, b.level, b.benefit_code, + b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip, + @vip_resource_contract_now_ms +FROM vip_level_benefits AS b +JOIN vip_program_configs AS p ON p.app_code = b.app_code; + +-- tiered Fami 三项房间视觉权益都属于 decoration,并使用严格的 code -> resource_type 映射。 +UPDATE vip_level_benefits +SET benefit_type = 'decoration', + resource_type = CASE benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + END, + updated_at_ms = @vip_resource_contract_now_ms +WHERE app_code = 'fami' + AND benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') + AND ( + benefit_type <> 'decoration' + OR resource_type <> CASE benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + END + ); + +SET @vip_decoration_normalize_count := ROW_COUNT(); + +UPDATE vip_level_benefits AS benefit +LEFT JOIN resources AS resource + ON resource.app_code = benefit.app_code + AND resource.resource_id = benefit.resource_id + AND resource.status = 'active' + AND resource.resource_type = CASE benefit.benefit_code + WHEN 'mic_skin' THEN 'mic_seat_animation' + WHEN 'room_border' THEN 'room_border' + WHEN 'colored_room_name' THEN 'room_name_style' + END +SET benefit.resource_id = 0, + benefit.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') + AND benefit.resource_id > 0 + AND resource.resource_id IS NULL; + +SET @vip_invalid_resource_clear_count := ROW_COUNT(); + +-- 只绑定资源组中唯一的 active 麦位动画。缺少真实目录资源的 VIP9、房间边框和房名样式保持未配置。 +UPDATE vip_level_benefits AS benefit +JOIN ( + SELECT resource_group.group_code, MIN(resource.resource_id) AS resource_id + FROM resource_groups AS resource_group + JOIN resource_group_items AS group_item + ON group_item.app_code = resource_group.app_code + AND group_item.group_id = resource_group.group_id + AND group_item.item_type = 'resource' + JOIN resources AS resource + ON resource.app_code = group_item.app_code + AND resource.resource_id = group_item.resource_id + AND resource.resource_type = 'mic_seat_animation' + AND resource.status = 'active' + WHERE resource_group.app_code = 'fami' + AND resource_group.status = 'active' + AND resource_group.group_code IN ('VIP4', 'VIP5', 'VIP6', 'VIP7', 'VIP8') + GROUP BY resource_group.group_code + HAVING COUNT(DISTINCT resource.resource_id) = 1 +) AS mapping + ON mapping.group_code = CONCAT('VIP', benefit.level) +SET benefit.resource_id = mapping.resource_id, + benefit.resource_type = 'mic_seat_animation', + benefit.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.level BETWEEN 4 AND 8 + AND benefit.benefit_code = 'mic_skin' + AND benefit.status = 'active' + AND benefit.resource_id = 0; + +SET @vip_mic_skin_bind_count := ROW_COUNT(); + +-- 只处理 active mic_skin 已绑定的真实麦座动画:合法 animation_format 优先;缺省时仅从 +-- animation/asset URL 的明确后缀推导 format。权益矩阵很小,资源表始终按主键点查。 +UPDATE vip_level_benefits AS benefit +JOIN resources AS resource FORCE INDEX (PRIMARY) + ON resource.resource_id = benefit.resource_id + AND resource.app_code = benefit.app_code + AND resource.resource_type = 'mic_seat_animation' + AND resource.status = 'active' +SET resource.metadata_json = JSON_SET( + COALESCE(resource.metadata_json, JSON_OBJECT()), + '$.format', + CASE + WHEN LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) + IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + THEN LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')))) + WHEN LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1)) + REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' + THEN CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1)) + WHEN 'jpg' THEN 'jpeg' + ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1)) + END + ELSE CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1)) + WHEN 'jpg' THEN 'jpeg' + ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1)) + END + END + ), + resource.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.benefit_code = 'mic_skin' + AND benefit.status = 'active' + AND benefit.resource_type = 'mic_seat_animation' + AND benefit.resource_id > 0 + AND (TRIM(resource.asset_url) <> '' OR TRIM(resource.animation_url) <> '') + AND TRIM(resource.preview_url) <> '' + AND LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), ''))) + NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + AND ( + LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) + IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1)) + REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' + OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1)) + REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' + ); + +SET @vip_mic_format_backfill_count := ROW_COUNT(); + +UPDATE vip_level_benefits AS benefit +LEFT JOIN resources AS resource + ON resource.app_code = benefit.app_code + AND resource.resource_id = benefit.resource_id + AND resource.status = 'active' + AND resource.resource_type = benefit.resource_type +SET benefit.status = 'disabled', + benefit.updated_at_ms = @vip_resource_contract_now_ms +WHERE benefit.app_code = 'fami' + AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') + AND benefit.status = 'active' + AND ( + resource.resource_id IS NULL + OR ( + benefit.resource_type IN ('mic_seat_animation', 'room_border') + AND ( + (TRIM(resource.asset_url) = '' AND TRIM(resource.animation_url) = '') + OR TRIM(resource.preview_url) = '' + OR LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), ''))) + NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') + OR ( + LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) <> '' + AND LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')))) + <> LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')))) + ) + ) + ) + ); + +SET @vip_decoration_disable_count := ROW_COUNT(); + +UPDATE vip_program_configs +SET config_version = config_version + 1, + updated_at_ms = @vip_resource_contract_now_ms +WHERE app_code = 'fami' + AND ( + @vip_decoration_normalize_count + + @vip_invalid_resource_clear_count + + @vip_mic_skin_bind_count + + @vip_mic_format_backfill_count + + @vip_decoration_disable_count + ) > 0; + +-- 若本次回填提升 config_version,同次执行就为最终版本落 baseline;下次重放开头的 +-- INSERT IGNORE 会直接命中唯一键,不会因为版本已提升而多追加一套相同快照。 +INSERT IGNORE INTO vip_benefit_resource_binding_history ( + app_code, history_id, config_version, change_phase, program_type, level, + benefit_code, benefit_status, trial_enabled, resource_id, resource_type, + auto_equip, recorded_at_ms +) +SELECT b.app_code, + SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256), + p.config_version, 'baseline', p.program_type, b.level, b.benefit_code, + b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip, + @vip_resource_contract_now_ms +FROM vip_level_benefits AS b +JOIN vip_program_configs AS p ON p.app_code = b.app_code; diff --git a/services/wallet-service/internal/domain/ledger/vip.go b/services/wallet-service/internal/domain/ledger/vip.go index db141c23..9c7faf3b 100644 --- a/services/wallet-service/internal/domain/ledger/vip.go +++ b/services/wallet-service/internal/domain/ledger/vip.go @@ -129,6 +129,38 @@ type VipBenefit struct { CreatedAtMS int64 UpdatedAtMS int64 Presentation *VipBenefitPresentation + // Resource 只承载已通过 App、状态和类型校验的真实目录资源;nil 明确表示当前不可渲染, + // 客户端不能再根据 benefit_code 或 VIP 等级拼接资源地址。 + Resource *VipBenefitResource +} + +// VipBenefitResource 是 wallet Resource 的完整只读投影。ledger 不能反向依赖 resource domain +// (resource domain 已依赖 ledger 的钱包资产常量),因此在 VIP 聚合内保留无行为的数据结构以避免循环依赖。 +type VipBenefitResource struct { + AppCode string + ResourceID int64 + ResourceCode string + ResourceType string + Name string + Status string + Grantable bool + ManagerGrantEnabled bool + GrantStrategy string + WalletAssetType string + WalletAssetAmount int64 + PriceType string + CoinPrice int64 + GiftPointAmount int64 + UsageScopes []string + AssetURL string + PreviewURL string + AnimationURL string + MetadataJSON string + SortOrder int32 + CreatedByUserID int64 + UpdatedByUserID int64 + CreatedAtMS int64 + UpdatedAtMS int64 } // VipBenefitPresentation 是 App 可直接渲染的权益表现契约。它从 metadata_json.presentation @@ -344,10 +376,14 @@ type GrantVipTrialCardReceipt struct { // EquipVipTrialCardCommand 以 entitlement 精确选择背包实例,避免同资源多卡时佩戴错对象。 type EquipVipTrialCardCommand struct { - AppCode string - RequestID string - UserID int64 - EntitlementID string + AppCode string + RequestID string + // EquipmentCommandID/ResourceID 仅由通用背包装备入口携带,用于把体验卡状态机的 + // 成功结果写入同一份装备幂等事实;专用旧接口保持为空,不改变原有 request_id 语义。 + EquipmentCommandID string + ResourceID int64 + UserID int64 + EntitlementID string } // UnequipVipTrialCardCommand 只清理 vip_trial_card 类型的装备事实,不回收或暂停任一卡片。 @@ -359,11 +395,13 @@ type UnequipVipTrialCardCommand struct { // CheckVipBenefitResult 是 owner service 执行单项特权时使用的服务端判断结果。 type CheckVipBenefitResult struct { - Allowed bool - Benefit VipBenefit - State VipState - DenialReason string - EvaluatedAtMS int64 + Allowed bool + Benefit VipBenefit + State VipState + DenialReason string + EvaluatedAtMS int64 + RequiredLevel int32 + CurrentEffectiveLevel int32 } // AdminVipLevelCommand 是后台保存 VIP 等级配置的完整事实输入。 diff --git a/services/wallet-service/internal/domain/resource/constants.go b/services/wallet-service/internal/domain/resource/constants.go index cdbb8dda..d80a1c6b 100644 --- a/services/wallet-service/internal/domain/resource/constants.go +++ b/services/wallet-service/internal/domain/resource/constants.go @@ -11,7 +11,11 @@ const ( TypeGift = "gift" TypeMicSeatIcon = "mic_seat_icon" TypeMicSeatAnimation = "mic_seat_animation" - TypeEmojiPack = "emoji_pack" + // TypeRoomBorder 与 TypeRoomNameStyle 是房间级装扮目录类型;应用动作必须进入 Room Cell, + // 不能复用 user_resource_equipment 把房间状态错误地挂到用户全局 appearance。 + TypeRoomBorder = "room_border" + TypeRoomNameStyle = "room_name_style" + TypeEmojiPack = "emoji_pack" // TypeVIPTrialCard 是可在背包中独立计时、单选佩戴的 VIP 体验卡。 // VIP 等级和有效期事实仍由 user_vip_trial_cards 保存,资源目录只负责素材和通用背包展示。 TypeVIPTrialCard = "vip_trial_card" @@ -36,10 +40,14 @@ const ( GrantSourceAchievement = "achievement" GrantSourceResourceShop = "resource_shop" GrantSourceGameRobotInit = "game_robot_init" - GrantStatusDone = "succeeded" - GrantStatusRevoked = "revoked" - ResultWalletCredit = "wallet_credit" - ResultEntitlement = "entitlement" + // GrantSourceVIPPaid 与 GrantSourceVIPTrial 把两种 VIP 生效来源的 entitlement + // 完全隔离;任一来源到期或撤销都不能缩短另一来源仍有效的装扮权益。 + GrantSourceVIPPaid = "vip_paid" + GrantSourceVIPTrial = "vip_trial" + GrantStatusDone = "succeeded" + GrantStatusRevoked = "revoked" + ResultWalletCredit = "wallet_credit" + ResultEntitlement = "entitlement" GroupItemTypeResource = "resource" GroupItemTypeWalletAsset = "wallet_asset" diff --git a/services/wallet-service/internal/domain/resource/entitlement.go b/services/wallet-service/internal/domain/resource/entitlement.go index 9fa50aae..5a576926 100644 --- a/services/wallet-service/internal/domain/resource/entitlement.go +++ b/services/wallet-service/internal/domain/resource/entitlement.go @@ -40,15 +40,24 @@ type RevokeUserResourceCommand struct { } type EquipUserResourceCommand struct { - // RequestID 在通用背包命中 VIP 体验卡时作为专用状态机的 outbox 幂等事件键; - // 普通装扮仍不把 request_id 当业务幂等键。 - RequestID string + // RequestID 只用于链路追踪;不能作为客户端网络重试的业务幂等键。 + RequestID string + // CommandID 是装备状态变更的持久幂等键。同键不同 payload 必须返回 IDEMPOTENCY_CONFLICT。 + CommandID string AppCode string UserID int64 ResourceID int64 EntitlementID string } +// VipBenefitRequirement 描述一个资源由 tiered VIP 权益解锁的最低门槛。 +// 同一资源若被多个权益引用,装备时满足任一 active effective benefit 即可。 +type VipBenefitRequirement struct { + BenefitCode string + RequiredLevel int32 + ResourceType string +} + type UnequipUserResourceCommand struct { // RequestID 仅在 vip_trial_card 委托专用卸下流程时用于发布完整 VIP 状态变更事件。 RequestID string diff --git a/services/wallet-service/internal/domain/resource/resource.go b/services/wallet-service/internal/domain/resource/resource.go index 236cc44e..64650b99 100644 --- a/services/wallet-service/internal/domain/resource/resource.go +++ b/services/wallet-service/internal/domain/resource/resource.go @@ -123,7 +123,7 @@ func NormalizeGrantSource(value string) string { func ValidResourceType(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { - case TypeAvatarFrame, TypeProfileCard, TypeCoin, TypeVehicle, TypeChatBubble, TypeBadge, TypeFloatingScreen, TypeGift, TypeMicSeatIcon, TypeMicSeatAnimation, TypeEmojiPack, TypeVIPTrialCard: + case TypeAvatarFrame, TypeProfileCard, TypeCoin, TypeVehicle, TypeChatBubble, TypeBadge, TypeFloatingScreen, TypeGift, TypeMicSeatIcon, TypeMicSeatAnimation, TypeRoomBorder, TypeRoomNameStyle, TypeEmojiPack, TypeVIPTrialCard: return true default: return false diff --git a/services/wallet-service/internal/service/wallet/ports/repository.go b/services/wallet-service/internal/service/wallet/ports/repository.go index 01384f68..3f065710 100644 --- a/services/wallet-service/internal/service/wallet/ports/repository.go +++ b/services/wallet-service/internal/service/wallet/ports/repository.go @@ -215,6 +215,8 @@ type ResourceGrantStore interface { // ResourceEquipmentStore 管理用户资源权益和穿戴状态。 type ResourceEquipmentStore interface { ListUserResources(ctx context.Context, query resourcedomain.ListUserResourcesQuery) ([]resourcedomain.UserResourceEntitlement, error) + ListVipBenefitRequirementsForResource(ctx context.Context, resourceID int64) ([]resourcedomain.VipBenefitRequirement, error) + ReplayEquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, bool, error) RevokeUserResource(ctx context.Context, command resourcedomain.RevokeUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) EquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error) diff --git a/services/wallet-service/internal/service/wallet/resource.go b/services/wallet-service/internal/service/wallet/resource.go index 3f0f4c1a..77ec67ba 100644 --- a/services/wallet-service/internal/service/wallet/resource.go +++ b/services/wallet-service/internal/service/wallet/resource.go @@ -2,6 +2,7 @@ package wallet import ( "context" + "strconv" "strings" "hyapp/pkg/appcode" @@ -298,7 +299,24 @@ func (s *Service) EquipUserResource(ctx context.Context, command resourcedomain. if command.UserID <= 0 || command.ResourceID <= 0 { return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "user_id and resource_id are required") } + command.RequestID = strings.TrimSpace(command.RequestID) + command.CommandID = strings.TrimSpace(command.CommandID) + if command.CommandID == "" { + // 兼容旧内部调用方:历史代码只传 request_id。新 HTTP 契约会显式传 command_id; + // 这里的回退只防止 Lalu/既有任务立即失效,不把 request_id 宣称为新的业务协议。 + command.CommandID = command.RequestID + } + if len(command.CommandID) > 128 { + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "command_id is too long") + } command.EntitlementID = strings.TrimSpace(command.EntitlementID) + if command.CommandID != "" { + // 完成命令必须在当前 entitlement/VIP 状态校验前重放;否则首请求成功后资源到期, + // 同 command_id 会错误地变成 NotFound/VIP_BENEFIT_REQUIRED,破坏幂等回包契约。 + if replayed, exists, err := s.repository.ReplayEquipUserResource(ctx, command); err != nil || exists { + return replayed, err + } + } items, err := s.repository.ListUserResources(ctx, resourcedomain.ListUserResourcesQuery{ AppCode: command.AppCode, UserID: command.UserID, ActiveOnly: true, }) @@ -315,22 +333,28 @@ func (s *Service) EquipUserResource(ctx context.Context, command resourcedomain. if selected == nil { return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "user resource entitlement not found") } + if err := s.requireVipBenefitForResourceEquip(ctx, command.UserID, command.ResourceID, selected.Resource.ResourceType); err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } if resourcedomain.NormalizeResourceType(selected.Resource.ResourceType) == resourcedomain.TypeVIPTrialCard { // 体验卡在 UI 上仍是普通背包物品,但其装备动作会改变最终 VIP 权益。 // 因此通用入口必须委托专用状态机,不能直接写 equipment 而绕过 App 开关和 VipEffectiveChanged outbox。 - command.RequestID = strings.TrimSpace(command.RequestID) command.EntitlementID = strings.TrimSpace(command.EntitlementID) - if command.RequestID == "" || command.EntitlementID == "" { - return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "request_id and entitlement_id are required for vip_trial_card") + if command.CommandID == "" || command.EntitlementID == "" { + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "command_id and entitlement_id are required for vip_trial_card") } if selected.EntitlementID != command.EntitlementID { return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "vip trial card entitlement not found") } if _, _, err := s.EquipVipTrialCard(ctx, ledger.EquipVipTrialCardCommand{ - AppCode: command.AppCode, - RequestID: command.RequestID, - UserID: command.UserID, - EntitlementID: command.EntitlementID, + AppCode: command.AppCode, + // 体验卡专用状态机的 RequestID 实际承担稳定 outbox event key;这里必须使用业务 command_id, + // 不能退回每次重试都会变化的 HTTP request_id。 + RequestID: command.CommandID, + EquipmentCommandID: command.CommandID, + ResourceID: command.ResourceID, + UserID: command.UserID, + EntitlementID: command.EntitlementID, }); err != nil { return resourcedomain.UserResourceEntitlement{}, err } @@ -341,6 +365,39 @@ func (s *Service) EquipUserResource(ctx context.Context, command resourcedomain. return s.repository.EquipUserResource(ctx, command) } +// requireVipBenefitForResourceEquip 对 tiered VIP 绑定资源执行服务端二次门禁。 +// repository 的绑定查询已联结 program_type,因此 Lalu legacy_timed 资源不会进入此分支; +// Fami 用户即使持有历史 entitlement,也必须仍有对应 effective benefit 才能重新装备。 +func (s *Service) requireVipBenefitForResourceEquip(ctx context.Context, userID int64, resourceID int64, resourceType string) error { + requirements, err := s.repository.ListVipBenefitRequirementsForResource(ctx, resourceID) + if err != nil || len(requirements) == 0 { + return err + } + var denied ledger.CheckVipBenefitResult + for _, requirement := range requirements { + result, checkErr := s.repository.CheckVipBenefit(ctx, userID, requirement.BenefitCode) + if checkErr != nil { + // 权益 owner 读取失败必须失败关闭,不能因为用户已有 entitlement 就跳过付费门禁。 + return checkErr + } + // 权益 code 相同仍不代表样式相同:VIP8 的旧 entitlement 在切到 VIP4 后可能尚未到期。 + // 只有 wallet 当前 effective benefit 精确指向本 resource_id/type 才允许重新装备。 + if result.Allowed && result.Benefit.ResourceID == resourceID && + resourcedomain.NormalizeResourceType(result.Benefit.ResourceType) == resourcedomain.NormalizeResourceType(resourceType) && + resourcedomain.NormalizeResourceType(requirement.ResourceType) == resourcedomain.NormalizeResourceType(resourceType) { + return nil + } + denied = result + } + requirement := requirements[0] + return xerr.NewWithMetadata(xerr.VIPBenefitRequired, "vip benefit is required for resource equip", map[string]string{ + "benefit_code": requirement.BenefitCode, + "required_level": strconv.FormatInt(int64(denied.RequiredLevel), 10), + "current_effective_level": strconv.FormatInt(int64(denied.CurrentEffectiveLevel), 10), + "action": "equip_resource", + }) +} + func (s *Service) UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error) { if s.repository == nil { return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") diff --git a/services/wallet-service/internal/service/wallet/vip.go b/services/wallet-service/internal/service/wallet/vip.go index 2e290bfc..cf594051 100644 --- a/services/wallet-service/internal/service/wallet/vip.go +++ b/services/wallet-service/internal/service/wallet/vip.go @@ -158,6 +158,7 @@ func (s *Service) GrantVipTrialCard(ctx context.Context, command ledger.GrantVip // EquipVipTrialCard 只切换通用装备指针,任一卡片的 effective/expires 时间都不会变化。 func (s *Service) EquipVipTrialCard(ctx context.Context, command ledger.EquipVipTrialCardCommand) (ledger.VipTrialCard, ledger.VipState, error) { command.RequestID = strings.TrimSpace(command.RequestID) + command.EquipmentCommandID = strings.TrimSpace(command.EquipmentCommandID) command.EntitlementID = strings.TrimSpace(command.EntitlementID) if command.RequestID == "" || command.UserID <= 0 || command.EntitlementID == "" { return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.InvalidArgument, "vip trial equip command is incomplete") @@ -165,6 +166,9 @@ func (s *Service) EquipVipTrialCard(ctx context.Context, command ledger.EquipVip if len(command.RequestID) > 128 || len(command.EntitlementID) > 96 { return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.InvalidArgument, "vip trial equip command is invalid") } + if command.EquipmentCommandID != "" && (len(command.EquipmentCommandID) > 128 || command.ResourceID <= 0) { + return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.InvalidArgument, "resource equip command is invalid") + } if s.repository == nil { return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Unavailable, "wallet repository is not configured") } diff --git a/services/wallet-service/internal/storage/mysql/repository.go b/services/wallet-service/internal/storage/mysql/repository.go index 2763011c..41313840 100644 --- a/services/wallet-service/internal/storage/mysql/repository.go +++ b/services/wallet-service/internal/storage/mysql/repository.go @@ -47,6 +47,10 @@ func Open(ctx context.Context, dsn string) (*Repository, error) { _ = db.Close() return nil, err } + if err := ensureVipBenefitResourceBindingHistorySchema(ctx, db); err != nil { + _ = db.Close() + return nil, err + } if err := ensureExternalRechargeSchema(ctx, db); err != nil { _ = db.Close() return nil, err diff --git a/services/wallet-service/internal/storage/mysql/resource_catalog_repository.go b/services/wallet-service/internal/storage/mysql/resource_catalog_repository.go index 622cab10..c2a8640d 100644 --- a/services/wallet-service/internal/storage/mysql/resource_catalog_repository.go +++ b/services/wallet-service/internal/storage/mysql/resource_catalog_repository.go @@ -6,14 +6,23 @@ import ( "encoding/json" "errors" "fmt" + "math" + "regexp" + "strings" + "time" + "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" resourcedomain "hyapp/services/wallet-service/internal/domain/resource" - "strings" - "time" ) +var roomNameResourceColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$`) + +var vipVisualResourceFormats = map[string]struct{}{ + "gif": {}, "jpeg": {}, "mp4": {}, "pag": {}, "png": {}, "svga": {}, "webp": {}, +} + // ListResources 返回后台资源库列表;App 入口会设置 active_only 只读上架资源。 func (r *Repository) ListResources(ctx context.Context, query resourcedomain.ListResourcesQuery) ([]resourcedomain.Resource, int64, error) { if r == nil || r.db == nil { @@ -585,6 +594,9 @@ func validateResourceCommand(command resourcedomain.ResourceCommand) error { if !resourcedomain.ValidGrantStrategy(command.GrantStrategy) { return xerr.New(xerr.InvalidArgument, "grant_strategy is invalid") } + if err := validateRoomDecorationResource(command); err != nil { + return err + } if command.ResourceType == resourcedomain.TypeCoin { if command.GrantStrategy != resourcedomain.GrantStrategyWalletCredit || command.WalletAssetType != ledger.AssetCoin || command.WalletAssetAmount <= 0 { return xerr.New(xerr.InvalidArgument, "coin resource must credit COIN") @@ -610,6 +622,65 @@ func validateResourceCommand(command resourcedomain.ResourceCommand) error { return nil } +func validateRoomDecorationResource(command resourcedomain.ResourceCommand) error { + switch command.ResourceType { + case resourcedomain.TypeMicSeatAnimation, resourcedomain.TypeRoomBorder: + // Room Cell/appearance 只保存资源快照,不会替运营生成素材;目录层拒绝空壳视觉资源, + // 同时固化格式,Flutter 才不需要从 URL 后缀猜 PAG、SVGA 或 MP4。 + if command.AssetURL == "" && command.AnimationURL == "" { + return xerr.New(xerr.InvalidArgument, "vip visual resource asset is required") + } + if command.PreviewURL == "" { + return xerr.New(xerr.InvalidArgument, "vip visual resource preview is required") + } + var metadata struct { + Format string `json:"format"` + AnimationFormat string `json:"animation_format"` + } + if err := json.Unmarshal([]byte(command.MetadataJSON), &metadata); err != nil { + return xerr.New(xerr.InvalidArgument, "vip visual resource metadata is invalid") + } + format := strings.ToLower(strings.TrimSpace(metadata.Format)) + if _, allowed := vipVisualResourceFormats[format]; !allowed { + return xerr.New(xerr.InvalidArgument, "vip visual resource format is invalid") + } + if animationFormat := strings.ToLower(strings.TrimSpace(metadata.AnimationFormat)); animationFormat != "" && animationFormat != format { + return xerr.New(xerr.InvalidArgument, "vip visual resource format mismatch") + } + return nil + case resourcedomain.TypeRoomNameStyle: + var metadata struct { + TextColors []string `json:"text_colors"` + Stops []float64 `json:"stops"` + AngleDegrees *float64 `json:"angle_degrees"` + } + if err := json.Unmarshal([]byte(command.MetadataJSON), &metadata); err != nil || len(metadata.TextColors) == 0 || len(metadata.TextColors) > 8 { + return xerr.New(xerr.InvalidArgument, "room name style colors are invalid") + } + for _, color := range metadata.TextColors { + if !roomNameResourceColorPattern.MatchString(strings.TrimSpace(color)) { + return xerr.New(xerr.InvalidArgument, "room name style color is invalid") + } + } + if len(metadata.Stops) != 0 && len(metadata.Stops) != len(metadata.TextColors) { + return xerr.New(xerr.InvalidArgument, "room name style stops are invalid") + } + previous := -1.0 + for _, stop := range metadata.Stops { + if math.IsNaN(stop) || math.IsInf(stop, 0) || stop < 0 || stop > 1 || stop < previous { + return xerr.New(xerr.InvalidArgument, "room name style stops are invalid") + } + previous = stop + } + if metadata.AngleDegrees != nil && (math.IsNaN(*metadata.AngleDegrees) || math.IsInf(*metadata.AngleDegrees, 0)) { + return xerr.New(xerr.InvalidArgument, "room name style angle is invalid") + } + return nil + default: + return nil + } +} + func mapResourceWriteError(err error) error { if isMySQLDuplicateError(err) { return xerr.New(xerr.Conflict, "resource_code already exists") diff --git a/services/wallet-service/internal/storage/mysql/resource_entitlement_repository.go b/services/wallet-service/internal/storage/mysql/resource_entitlement_repository.go index ab460f1c..eb45b5b0 100644 --- a/services/wallet-service/internal/storage/mysql/resource_entitlement_repository.go +++ b/services/wallet-service/internal/storage/mysql/resource_entitlement_repository.go @@ -64,6 +64,91 @@ func (r *Repository) ListUserResources(ctx context.Context, query resourcedomain return items, rows.Err() } +// ListVipBenefitRequirementsForResource 查询资源在 tiered VIP 中的当前或历史显式绑定。 +// program disabled、权益删除和 A->B 改绑都不能让 A 退化成普通资源;是否当前可用仍由 +// CheckVipBenefit 与仓储事务内 exact-binding 门禁判定。legacy_timed 快照始终排除。 +func (r *Repository) ListVipBenefitRequirementsForResource(ctx context.Context, resourceID int64) ([]resourcedomain.VipBenefitRequirement, error) { + if r == nil || r.db == nil { + return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + if resourceID <= 0 { + return nil, xerr.New(xerr.InvalidArgument, "resource_id is required") + } + rows, err := r.db.QueryContext(ctx, ` + SELECT binding.benefit_code, MIN(binding.level), binding.resource_type + FROM ( + SELECT b.benefit_code, b.level, b.resource_type + FROM vip_level_benefits AS b + JOIN vip_program_configs AS p ON p.app_code = b.app_code + AND p.program_type = ? + WHERE b.app_code = ? AND b.resource_id = ? + UNION ALL + SELECT h.benefit_code, h.level, h.resource_type + FROM vip_benefit_resource_binding_history AS h + WHERE h.app_code = ? AND h.resource_id = ? AND h.program_type = ? + ) AS binding + GROUP BY binding.benefit_code, binding.resource_type + ORDER BY MIN(binding.level) ASC, binding.benefit_code ASC`, + ledger.VipProgramTypeTieredPrivilegeV1, + appcode.FromContext(ctx), resourceID, + appcode.FromContext(ctx), resourceID, ledger.VipProgramTypeTieredPrivilegeV1, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := make([]resourcedomain.VipBenefitRequirement, 0, 1) + for rows.Next() { + var item resourcedomain.VipBenefitRequirement + if err := rows.Scan(&item.BenefitCode, &item.RequiredLevel, &item.ResourceType); err != nil { + return nil, err + } + items = append(items, item) + } + return items, rows.Err() +} + +// ReplayEquipUserResource 在任何当前状态校验前按主键读取已完成命令。 +// 该查询只触达 append-only command 表;未命中时首次/并发请求继续进入事务 guard 收敛。 +func (r *Repository) ReplayEquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, bool, error) { + if r == nil || r.db == nil { + return resourcedomain.UserResourceEntitlement{}, false, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + ctx = contextWithCommandApp(ctx, command.AppCode) + command.AppCode = appcode.FromContext(ctx) + command.CommandID = strings.TrimSpace(command.CommandID) + command.EntitlementID = strings.TrimSpace(command.EntitlementID) + if command.CommandID == "" { + return resourcedomain.UserResourceEntitlement{}, false, nil + } + var storedHash string + var responseJSON sql.NullString + err := r.db.QueryRowContext(ctx, ` + SELECT request_hash, CAST(response_json AS CHAR) + FROM user_resource_equipment_commands + WHERE app_code = ? AND command_id = ?`, command.AppCode, command.CommandID, + ).Scan(&storedHash, &responseJSON) + if errors.Is(err, sql.ErrNoRows) { + return resourcedomain.UserResourceEntitlement{}, false, nil + } + if err != nil { + return resourcedomain.UserResourceEntitlement{}, false, err + } + if storedHash != resourceEquipmentRequestHash(command) { + return resourcedomain.UserResourceEntitlement{}, false, xerr.New(xerr.IdempotencyConflict, "resource equip command payload conflict") + } + if !responseJSON.Valid || strings.TrimSpace(responseJSON.String) == "" { + // committed command rows always contain response_json;空值仅作为并发首次事务内的临时状态, + // 普通读看不到未提交行,因此这里防御性地继续事务 guard。 + return resourcedomain.UserResourceEntitlement{}, false, nil + } + var replayed resourcedomain.UserResourceEntitlement + if err := json.Unmarshal([]byte(responseJSON.String), &replayed); err != nil { + return resourcedomain.UserResourceEntitlement{}, false, xerr.New(xerr.Internal, "resource equip command response is invalid") + } + return replayed, true, nil +} + // RevokeUserResource forcibly invalidates exactly one entitlement. It deliberately does not mutate // the source grant because a resource-group grant can contain sibling assets that must stay active. func (r *Repository) RevokeUserResource(ctx context.Context, command resourcedomain.RevokeUserResourceCommand) (resourcedomain.UserResourceEntitlement, error) { @@ -173,6 +258,7 @@ func (r *Repository) EquipUserResource(ctx context.Context, command resourcedoma } ctx = contextWithCommandApp(ctx, command.AppCode) command.AppCode = appcode.FromContext(ctx) + command.CommandID = strings.TrimSpace(command.CommandID) command.EntitlementID = strings.TrimSpace(command.EntitlementID) if command.UserID <= 0 || command.ResourceID <= 0 { return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.InvalidArgument, "user_id and resource_id are required") @@ -185,6 +271,20 @@ func (r *Repository) EquipUserResource(ctx context.Context, command resourcedoma defer func() { _ = tx.Rollback() }() nowMs := time.Now().UnixMilli() + if command.CommandID != "" { + replayed, exists, err := r.lockResourceEquipmentCommand(ctx, tx, command, nowMs) + if err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + if exists { + return replayed, nil + } + } + // 幂等 guard 之后、entitlement/equipment 写锁之前共享锁 VIP program,并在同一事务重做 + // exact resource binding 校验;admin 改绑事务不能再从 service 预检与最终写入之间穿透。 + if err := r.requireCurrentVipResourceBindingForUserTx(ctx, tx, command.UserID, command.ResourceID, nowMs); err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } entitlement, err := r.queryEquipableEntitlementForUpdate(ctx, tx, command, nowMs) if err != nil { return resourcedomain.UserResourceEntitlement{}, err @@ -201,6 +301,10 @@ func (r *Repository) EquipUserResource(ctx context.Context, command resourcedoma } entitlement.Equipped = true eventCommandID := fmt.Sprintf("equip:%s:%d", entitlement.EntitlementID, time.Now().UnixNano()) + if command.CommandID != "" { + // command_id 已在同一事务的 guard 行中锁定,outbox 也复用稳定键,保证 HTTP 超时重试不重复投递装扮变化。 + eventCommandID = boundedDerivedCommandID("equip:", command.CommandID) + } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ resourceOutboxEvent("UserResourceChanged", eventCommandID, command.UserID, command.ResourceID, map[string]any{ "action": "equip", @@ -215,12 +319,86 @@ func (r *Repository) EquipUserResource(ctx context.Context, command resourcedoma }); err != nil { return resourcedomain.UserResourceEntitlement{}, err } + if command.CommandID != "" { + if err := r.completeResourceEquipmentCommandTx(ctx, tx, command, entitlement, nowMs); err != nil { + return resourcedomain.UserResourceEntitlement{}, err + } + } if err := tx.Commit(); err != nil { return resourcedomain.UserResourceEntitlement{}, err } return entitlement, nil } +// lockResourceEquipmentCommand 在 entitlement/equipment 业务锁之前占用幂等键。 +// 同键并发请求会等待首事务提交,然后读取不可变 response_json;payload 不同则稳定返回冲突。 +func (r *Repository) lockResourceEquipmentCommand(ctx context.Context, tx *sql.Tx, command resourcedomain.EquipUserResourceCommand, nowMs int64) (resourcedomain.UserResourceEntitlement, bool, error) { + requestHash := resourceEquipmentRequestHash(command) + if _, err := tx.ExecContext(ctx, ` + INSERT INTO user_resource_equipment_commands ( + app_code, command_id, request_hash, user_id, resource_id, requested_entitlement_id, + result_entitlement_id, resource_type, response_json, created_at_ms, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, '', '', NULL, ?, ?) + ON DUPLICATE KEY UPDATE command_id = VALUES(command_id)`, + command.AppCode, command.CommandID, requestHash, command.UserID, command.ResourceID, + command.EntitlementID, nowMs, nowMs, + ); err != nil { + return resourcedomain.UserResourceEntitlement{}, false, err + } + var storedHash string + var responseJSON sql.NullString + if err := tx.QueryRowContext(ctx, ` + SELECT request_hash, CAST(response_json AS CHAR) + FROM user_resource_equipment_commands + WHERE app_code = ? AND command_id = ? + FOR UPDATE`, command.AppCode, command.CommandID, + ).Scan(&storedHash, &responseJSON); err != nil { + return resourcedomain.UserResourceEntitlement{}, false, err + } + if storedHash != requestHash { + return resourcedomain.UserResourceEntitlement{}, false, xerr.New(xerr.IdempotencyConflict, "resource equip command payload conflict") + } + if !responseJSON.Valid || strings.TrimSpace(responseJSON.String) == "" { + return resourcedomain.UserResourceEntitlement{}, false, nil + } + var replayed resourcedomain.UserResourceEntitlement + if err := json.Unmarshal([]byte(responseJSON.String), &replayed); err != nil { + return resourcedomain.UserResourceEntitlement{}, false, xerr.New(xerr.Internal, "resource equip command response is invalid") + } + return replayed, true, nil +} + +func resourceEquipmentRequestHash(command resourcedomain.EquipUserResourceCommand) string { + return stableHash(fmt.Sprintf("resource_equip|%s|%d|%d|%s", appcode.Normalize(command.AppCode), command.UserID, command.ResourceID, strings.TrimSpace(command.EntitlementID))) +} + +// completeResourceEquipmentCommandTx 与装备关系、VIP 体验卡切换共用同一事务提交回包快照。 +// 因而资源随后到期或 VIP 等级变化时,相同 command_id 仍能在所有当前状态校验前原样重放。 +func (r *Repository) completeResourceEquipmentCommandTx(ctx context.Context, tx *sql.Tx, command resourcedomain.EquipUserResourceCommand, entitlement resourcedomain.UserResourceEntitlement, nowMS int64) error { + responseJSON, err := json.Marshal(entitlement) + if err != nil { + return err + } + result, err := tx.ExecContext(ctx, ` + UPDATE user_resource_equipment_commands + SET result_entitlement_id = ?, resource_type = ?, response_json = ?, updated_at_ms = ? + WHERE app_code = ? AND command_id = ?`, + entitlement.EntitlementID, entitlement.Resource.ResourceType, responseJSON, nowMS, + command.AppCode, command.CommandID, + ) + if err != nil { + return err + } + affected, err := result.RowsAffected() + if err != nil { + return err + } + if affected != 1 { + return xerr.New(xerr.Internal, "resource equip command completion is missing") + } + return nil +} + func (r *Repository) UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error) { if r == nil || r.db == nil { return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured") @@ -361,6 +539,14 @@ func (r *Repository) BatchGetUserEquippedResources(ctx context.Context, query re if err := rows.Err(); err != nil { return nil, err } + if err := rows.Close(); err != nil { + return nil, err + } + // appearance 是高频批量读,不能逐用户调用 CheckVipBenefit。这里用固定三次、全部走 + // app/user/resource 覆盖索引的批查询,过滤掉改绑、删除、program disabled 后的历史 VIP 装备。 + if err := r.filterCurrentVipEquipmentBatch(ctx, userIDs, grouped, nowMs); err != nil { + return nil, err + } result := make([]resourcedomain.UserEquippedResources, 0, len(userIDs)) for _, userID := range userIDs { @@ -471,6 +657,15 @@ func (r *Repository) autoEquipGrantedEntitlementTx(ctx context.Context, tx *sql. if !isAutoEquippedOnGrantResourceType(resource.ResourceType) || strings.TrimSpace(entitlementID) == "" { return nil } + vipBound, err := r.isTieredVipBenefitResourceTx(ctx, tx, resource.ResourceID) + if err != nil { + return err + } + if vipBound { + // 目录资源只要曾属于 tiered VIP,就不能再由 generic grant 按类型自动装备;否则 + // trial_enabled=false 或 benefit.auto_equip=false 会被 mic_seat_animation 默认值绕过。 + return nil + } entitlement, err := r.getUserResourceEntitlementTx(ctx, tx, entitlementID) if err != nil { return err diff --git a/services/wallet-service/internal/storage/mysql/resource_grant_repository.go b/services/wallet-service/internal/storage/mysql/resource_grant_repository.go index 1a2ce542..12b69294 100644 --- a/services/wallet-service/internal/storage/mysql/resource_grant_repository.go +++ b/services/wallet-service/internal/storage/mysql/resource_grant_repository.go @@ -448,6 +448,16 @@ type resourceEntitlementSnapshot struct { } func (r *Repository) applyGrantItemWithEntitlementSnapshot(ctx context.Context, tx *sql.Tx, grantID string, commandID string, targetUserID int64, resource resourcedomain.Resource, quantity int64, durationMS int64, nowMs int64, entitlementStrategy string, entitlementSnapshot resourceEntitlementSnapshot) (resourcedomain.ResourceGrantItem, error) { + return r.applyGrantItemWithEntitlementSnapshotOptions(ctx, tx, grantID, commandID, targetUserID, resource, quantity, durationMS, nowMs, entitlementStrategy, entitlementSnapshot, true) +} + +// applyVipGrantItemWithoutAutoEquipTx 只创建来源独立的 VIP entitlement。VIP 的 auto_equip +// 是 benefit 配置,不是 resource_type 默认值;最终装备必须统一交给 effective-state reconcile。 +func (r *Repository) applyVipGrantItemWithoutAutoEquipTx(ctx context.Context, tx *sql.Tx, grantID string, commandID string, targetUserID int64, resource resourcedomain.Resource, quantity int64, durationMS int64, nowMs int64) (resourcedomain.ResourceGrantItem, error) { + return r.applyGrantItemWithEntitlementSnapshotOptions(ctx, tx, grantID, commandID, targetUserID, resource, quantity, durationMS, nowMs, resourcedomain.GrantStrategyNewEntitlement, resourceEntitlementSnapshot{}, false) +} + +func (r *Repository) applyGrantItemWithEntitlementSnapshotOptions(ctx context.Context, tx *sql.Tx, grantID string, commandID string, targetUserID int64, resource resourcedomain.Resource, quantity int64, durationMS int64, nowMs int64, entitlementStrategy string, entitlementSnapshot resourceEntitlementSnapshot, autoEquip bool) (resourcedomain.ResourceGrantItem, error) { if quantity <= 0 { return resourcedomain.ResourceGrantItem{}, xerr.New(xerr.InvalidArgument, "quantity must be positive") } @@ -521,8 +531,10 @@ func (r *Repository) applyGrantItemWithEntitlementSnapshot(ctx context.Context, } item.ResultType = resourcedomain.ResultEntitlement item.EntitlementID = entitlementID - if err := r.autoEquipGrantedEntitlementTx(ctx, tx, targetUserID, resource, entitlementID, nowMs); err != nil { - return resourcedomain.ResourceGrantItem{}, err + if autoEquip { + if err := r.autoEquipGrantedEntitlementTx(ctx, tx, targetUserID, resource, entitlementID, nowMs); err != nil { + return resourcedomain.ResourceGrantItem{}, err + } } } inserted, err := r.insertResourceGrantItem(ctx, tx, item) diff --git a/services/wallet-service/internal/storage/mysql/schema.go b/services/wallet-service/internal/storage/mysql/schema.go index e602e671..27117f8e 100644 --- a/services/wallet-service/internal/storage/mysql/schema.go +++ b/services/wallet-service/internal/storage/mysql/schema.go @@ -370,13 +370,75 @@ func ensureUserResourceEquipmentSchema(ctx context.Context, db *sql.DB) error { return err } if strings.Join(primaryColumns, ",") == "app_code,user_id,resource_type" { - _, err = db.ExecContext(ctx, ` + if _, err = db.ExecContext(ctx, ` ALTER TABLE user_resource_equipment DROP PRIMARY KEY, - ADD PRIMARY KEY (app_code, user_id, resource_type, entitlement_id)`) + ADD PRIMARY KEY (app_code, user_id, resource_type, entitlement_id)`); err != nil { + return err + } + } + + // 命令表是 append-only 幂等事实,主键查询不会扫描 equipment 或 entitlement 大表。 + // response_json 固化首个成功回包,使资源随后到期/下架也不会改变同 command_id 的重试语义。 + _, err = db.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS user_resource_equipment_commands ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + command_id VARCHAR(128) NOT NULL COMMENT '装备业务幂等 ID', + request_hash CHAR(64) NOT NULL COMMENT '用户、资源和指定 entitlement 的请求语义哈希', + user_id BIGINT NOT NULL COMMENT '操作用户 ID', + resource_id BIGINT NOT NULL COMMENT '目标资源 ID', + requested_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '客户端指定的 entitlement;空表示服务端选择', + result_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首个成功命中的 entitlement', + resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '首个成功结果的资源类型', + response_json JSON NULL COMMENT '首个成功 UserResourceEntitlement 回包快照', + created_at_ms BIGINT NOT NULL COMMENT '首次命令时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '结果完成时间,UTC epoch ms', + PRIMARY KEY (app_code, command_id) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备命令幂等事实表'`) + return err +} + +// ensureVipBenefitResourceBindingHistorySchema 保证任何曾被 tiered VIP 绑定的资源都不会在 +// 后台改绑、删除权益或关闭 program 后退化成普通资源。历史表只随小型配置矩阵追加,运行时 +// 装备门禁按 (app_code,resource_id) 覆盖索引点查,不扫描用户 entitlement/equipment 流水。 +func ensureVipBenefitResourceBindingHistorySchema(ctx context.Context, db *sql.DB) error { + if _, err := db.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS vip_benefit_resource_binding_history ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', + history_id CHAR(64) NOT NULL COMMENT '配置版本、阶段、等级和权益的稳定哈希', + config_version BIGINT NOT NULL COMMENT '写入该快照的 VIP 配置版本', + change_phase VARCHAR(16) NOT NULL COMMENT 'baseline/before/after', + program_type VARCHAR(32) NOT NULL COMMENT '快照时的 VIP program 类型', + level INT NOT NULL COMMENT 'VIP 等级', + benefit_code VARCHAR(64) NOT NULL COMMENT '权益编码', + benefit_status VARCHAR(32) NOT NULL COMMENT '权益快照状态', + trial_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '体验卡是否可用快照', + resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '当时绑定资源 ID;0 表示未绑定', + resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '当时要求的资源类型', + auto_equip BOOLEAN NOT NULL DEFAULT FALSE COMMENT '当时自动装备策略', + recorded_at_ms BIGINT NOT NULL COMMENT '记录时间,UTC epoch ms', + PRIMARY KEY (app_code, history_id), + UNIQUE KEY uk_vip_binding_history_snapshot (app_code, config_version, change_phase, level, benefit_code), + KEY idx_vip_binding_history_resource (app_code, resource_id, program_type, benefit_code, level) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 权益资源绑定不可变历史'`); err != nil { return err } - return nil + // baseline 只复制当前配置小表,并通过唯一键保持启动幂等。历史资源从此即使被 DELETE + // 也可被装备/appearance 门禁识别;不触碰任何用户级大表。 + _, err := db.ExecContext(ctx, ` + INSERT IGNORE INTO vip_benefit_resource_binding_history ( + app_code, history_id, config_version, change_phase, program_type, level, + benefit_code, benefit_status, trial_enabled, resource_id, resource_type, + auto_equip, recorded_at_ms + ) + SELECT b.app_code, + SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256), + p.config_version, 'baseline', p.program_type, b.level, b.benefit_code, + b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip, + GREATEST(b.updated_at_ms, p.updated_at_ms) + FROM vip_level_benefits AS b + JOIN vip_program_configs AS p ON p.app_code = b.app_code`) + return err } func tablePrimaryKeyColumns(ctx context.Context, db *sql.DB, tableName string) ([]string, error) { diff --git a/services/wallet-service/internal/storage/mysql/utils.go b/services/wallet-service/internal/storage/mysql/utils.go index e69cf32b..6d759af8 100644 --- a/services/wallet-service/internal/storage/mysql/utils.go +++ b/services/wallet-service/internal/storage/mysql/utils.go @@ -61,6 +61,17 @@ func stableHash(value string) string { return hex.EncodeToString(sum[:]) } +// boundedDerivedCommandID 为内部子动作保留可读前缀,同时保证最终键不超过所有 +// command_id VARCHAR(128) 的共同边界。正常短键保持历史值,极长客户端键才稳定收敛为哈希。 +func boundedDerivedCommandID(prefix string, commandID string) string { + commandID = strings.TrimSpace(commandID) + derived := prefix + commandID + if len(derived) <= 128 { + return derived + } + return prefix + stableHash(commandID) +} + func mustJSON(value any) string { body, err := json.Marshal(value) if err != nil { diff --git a/services/wallet-service/internal/storage/mysql/vip_admin_repository.go b/services/wallet-service/internal/storage/mysql/vip_admin_repository.go index ba5253ba..b282c445 100644 --- a/services/wallet-service/internal/storage/mysql/vip_admin_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_admin_repository.go @@ -45,6 +45,16 @@ func (r *Repository) UpdateAdminVipLevels(ctx context.Context, levels []ledger.A } nowMS := time.Now().UnixMilli() + newConfigVersion := program.ConfigVersion + 1 + oldResourceIDs, err := r.listCurrentVipBenefitResourceIDsTx(ctx, tx) + if err != nil { + return ledger.VipProgramConfig{}, nil, err + } + // 先保存完整旧矩阵,再执行先删后插;即使运营移除权益或把 A 改绑 B,A 仍永久保留 + // tiered VIP 身份,后续装备和 appearance 都不会把旧 entitlement 当作普通资源放行。 + if err := r.recordVipBenefitResourceBindingSnapshotTx(ctx, tx, newConfigVersion, "before", program.ProgramType, nowMS); err != nil { + return ledger.VipProgramConfig{}, nil, err + } for _, level := range normalized { if level.Status == ledger.VipStatusActive && level.RewardResourceGroupID > 0 { if err := r.requireActiveResourceGroup(ctx, tx, level.RewardResourceGroupID); err != nil { @@ -71,7 +81,7 @@ func (r *Repository) UpdateAdminVipLevels(ctx context.Context, levels []ledger.A return ledger.VipProgramConfig{}, nil, err } for _, benefit := range level.Benefits { - if err := r.validateAdminVipBenefit(ctx, tx, level.Level, benefit); err != nil { + if err := r.validateAdminVipBenefit(ctx, tx, program, level.Level, benefit); err != nil { return ledger.VipProgramConfig{}, nil, err } var metadata any @@ -93,10 +103,28 @@ func (r *Repository) UpdateAdminVipLevels(ctx context.Context, levels []ledger.A } } } + newResourceIDs, err := r.listCurrentVipBenefitResourceIDsTx(ctx, tx) + if err != nil { + return ledger.VipProgramConfig{}, nil, err + } + if err := r.recordVipBenefitResourceBindingSnapshotTx(ctx, tx, newConfigVersion, "after", program.ProgramType, nowMS); err != nil { + return ledger.VipProgramConfig{}, nil, err + } if _, err := tx.ExecContext(ctx, ` - UPDATE vip_program_configs - SET config_version = config_version + 1, updated_by_admin_id = ?, updated_at_ms = ? - WHERE app_code = ?`, operatorUserID, nowMS, appcode.FromContext(ctx)); err != nil { + UPDATE vip_program_configs + SET config_version = ?, updated_by_admin_id = ?, updated_at_ms = ? + WHERE app_code = ?`, newConfigVersion, operatorUserID, nowMS, appcode.FromContext(ctx)); err != nil { + return ledger.VipProgramConfig{}, nil, err + } + changeCommandID := boundedDerivedCommandID("vip_program_changed:", strconv.FormatInt(newConfigVersion, 10)) + if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ + resourceOutboxEvent("VipProgramChanged", changeCommandID, 0, 0, map[string]any{ + "app_code": appcode.FromContext(ctx), "config_version": newConfigVersion, + "change_scope": "levels", "operator_user_id": operatorUserID, + "old_resource_ids": oldResourceIDs, "new_resource_ids": newResourceIDs, + "updated_at_ms": nowMS, + }, nowMS), + }); err != nil { return ledger.VipProgramConfig{}, nil, err } if err := tx.Commit(); err != nil { @@ -122,13 +150,26 @@ func (r *Repository) UpdateAdminVipProgramConfig(ctx context.Context, command le defer func() { _ = tx.Rollback() }() nowMS := time.Now().UnixMilli() + oldConfig, oldConfigErr := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` FOR UPDATE`, appcode.FromContext(ctx))) + if oldConfigErr != nil && !errors.Is(oldConfigErr, sql.ErrNoRows) { + return ledger.VipProgramConfig{}, oldConfigErr + } + newConfigVersion := int64(1) + oldProgramType := config.ProgramType + if oldConfigErr == nil { + newConfigVersion = oldConfig.ConfigVersion + 1 + oldProgramType = oldConfig.ProgramType + if err := r.recordVipBenefitResourceBindingSnapshotTx(ctx, tx, newConfigVersion, "before", oldProgramType, nowMS); err != nil { + return ledger.VipProgramConfig{}, err + } + } if _, err := tx.ExecContext(ctx, ` INSERT INTO vip_program_configs ( app_code, program_type, level_count, same_level_expiry_policy, upgrade_expiry_policy, downgrade_purchase_policy, benefit_inheritance_policy, grant_mode, trial_card_enabled, status, config_version, updated_by_admin_id, created_at_ms, updated_at_ms - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE program_type = VALUES(program_type), level_count = VALUES(level_count), same_level_expiry_policy = VALUES(same_level_expiry_policy), @@ -141,7 +182,7 @@ func (r *Repository) UpdateAdminVipProgramConfig(ctx context.Context, command le appcode.FromContext(ctx), config.ProgramType, config.LevelCount, config.SameLevelExpiryPolicy, config.UpgradeExpiryPolicy, config.DowngradePurchasePolicy, config.BenefitInheritancePolicy, config.GrantMode, config.TrialCardEnabled, - config.Status, command.OperatorUserID, nowMS, nowMS, + config.Status, newConfigVersion, command.OperatorUserID, nowMS, nowMS, ); err != nil { return ledger.VipProgramConfig{}, err } @@ -164,12 +205,78 @@ func (r *Repository) UpdateAdminVipProgramConfig(ctx context.Context, command le if err != nil { return ledger.VipProgramConfig{}, err } + if err := r.recordVipBenefitResourceBindingSnapshotTx(ctx, tx, config.ConfigVersion, "after", config.ProgramType, nowMS); err != nil { + return ledger.VipProgramConfig{}, err + } + resourceIDs, err := r.listCurrentVipBenefitResourceIDsTx(ctx, tx) + if err != nil { + return ledger.VipProgramConfig{}, err + } + changeCommandID := boundedDerivedCommandID("vip_program_changed:", strconv.FormatInt(config.ConfigVersion, 10)) + if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ + resourceOutboxEvent("VipProgramChanged", changeCommandID, 0, 0, map[string]any{ + "app_code": appcode.FromContext(ctx), "config_version": config.ConfigVersion, + "change_scope": "program", "program_type": config.ProgramType, + "status": config.Status, "level_count": config.LevelCount, + "trial_card_enabled": config.TrialCardEnabled, + "operator_user_id": command.OperatorUserID, "resource_ids": resourceIDs, + "previous_program_type": oldProgramType, "updated_at_ms": nowMS, + }, nowMS), + }); err != nil { + return ledger.VipProgramConfig{}, err + } if err := tx.Commit(); err != nil { return ledger.VipProgramConfig{}, err } return config, nil } +// recordVipBenefitResourceBindingSnapshotTx 只复制当前 App 的小型权益矩阵。history_id 与唯一键 +// 都由 config_version+phase+level+benefit_code 决定,事务重试不会产生重复历史。 +func (r *Repository) recordVipBenefitResourceBindingSnapshotTx(ctx context.Context, tx *sql.Tx, configVersion int64, phase string, programType string, nowMS int64) error { + phase = strings.TrimSpace(phase) + programType = strings.TrimSpace(programType) + if configVersion <= 0 || (phase != "before" && phase != "after") || programType == "" { + return xerr.New(xerr.Internal, "vip benefit binding snapshot context is invalid") + } + _, err := tx.ExecContext(ctx, ` + INSERT INTO vip_benefit_resource_binding_history ( + app_code, history_id, config_version, change_phase, program_type, level, + benefit_code, benefit_status, trial_enabled, resource_id, resource_type, + auto_equip, recorded_at_ms + ) + SELECT b.app_code, + SHA2(CONCAT_WS('|', b.app_code, ?, ?, b.level, b.benefit_code), 256), + ?, ?, ?, b.level, b.benefit_code, b.status, b.trial_enabled, + b.resource_id, b.resource_type, b.auto_equip, ? + FROM vip_level_benefits AS b + WHERE b.app_code = ?`, + configVersion, phase, configVersion, phase, programType, nowMS, appcode.FromContext(ctx), + ) + return err +} + +func (r *Repository) listCurrentVipBenefitResourceIDsTx(ctx context.Context, tx *sql.Tx) ([]int64, error) { + rows, err := tx.QueryContext(ctx, ` + SELECT DISTINCT resource_id + FROM vip_level_benefits + WHERE app_code = ? AND resource_id > 0 + ORDER BY resource_id ASC`, appcode.FromContext(ctx)) + if err != nil { + return nil, err + } + defer rows.Close() + resourceIDs := make([]int64, 0, 8) + for rows.Next() { + var resourceID int64 + if err := rows.Scan(&resourceID); err != nil { + return nil, err + } + resourceIDs = append(resourceIDs, resourceID) + } + return resourceIDs, rows.Err() +} + func normalizeAdminVipLevelCommands(levels []ledger.AdminVipLevelCommand, program ledger.VipProgramConfig) ([]ledger.AdminVipLevelCommand, error) { if int32(len(levels)) != program.LevelCount { return nil, xerr.New(xerr.InvalidArgument, "vip config level count does not match program") @@ -250,7 +357,7 @@ func validateAdminVipLevelCommand(level ledger.AdminVipLevelCommand, program led return nil } -func (r *Repository) validateAdminVipBenefit(ctx context.Context, tx *sql.Tx, level int32, benefit ledger.VipBenefit) error { +func (r *Repository) validateAdminVipBenefit(ctx context.Context, tx *sql.Tx, program ledger.VipProgramConfig, level int32, benefit ledger.VipBenefit) error { if benefit.BenefitCode == "" || benefit.Name == "" || benefit.UnlockLevel < 0 || benefit.ResourceID < 0 { return xerr.New(xerr.InvalidArgument, "vip benefit is incomplete") } @@ -316,6 +423,31 @@ func (r *Repository) validateAdminVipBenefit(ctx context.Context, tx *sql.Tx, le } } } + // Fami tiered 的三项房间视觉权益是稳定跨服务契约,不能由后台把 code 改成 + // function 或绑定到近似类型。disabled 可以暂不选素材,但一旦携带 resource_id, + // 下方仍会校验资源 active 且类型精确匹配,避免脏绑定在之后切状态时直接生效。 + expectedResourceType := "" + if program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 && appcode.FromContext(ctx) == "fami" { + switch benefit.BenefitCode { + case ledger.VipBenefitCodeMicSkin: + expectedResourceType = resourcedomain.TypeMicSeatAnimation + case ledger.VipBenefitCodeRoomBorder: + expectedResourceType = resourcedomain.TypeRoomBorder + case ledger.VipBenefitCodeColoredRoomName: + expectedResourceType = resourcedomain.TypeRoomNameStyle + } + } + if expectedResourceType != "" { + if benefit.BenefitType != ledger.VipBenefitTypeDecoration { + return xerr.New(xerr.InvalidArgument, "vip room decoration benefit_type must be decoration") + } + if benefit.ResourceType != expectedResourceType { + return xerr.New(xerr.InvalidArgument, "vip room decoration resource_type mismatch") + } + if benefit.Status == ledger.VipStatusActive && benefit.ResourceID <= 0 { + return xerr.New(xerr.InvalidArgument, "active vip room decoration requires a resource") + } + } if benefit.ResourceID == 0 { return nil } diff --git a/services/wallet-service/internal/storage/mysql/vip_benefit_resource_repository.go b/services/wallet-service/internal/storage/mysql/vip_benefit_resource_repository.go new file mode 100644 index 00000000..5c1d2003 --- /dev/null +++ b/services/wallet-service/internal/storage/mysql/vip_benefit_resource_repository.go @@ -0,0 +1,772 @@ +package mysql + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "sort" + "strings" + + "hyapp/pkg/appcode" + "hyapp/pkg/xerr" + "hyapp/services/wallet-service/internal/domain/ledger" + resourcedomain "hyapp/services/wallet-service/internal/domain/resource" +) + +type activeVipBenefitResource struct { + BenefitCode string + AutoEquip bool + Resource resourcedomain.Resource +} + +// listActiveVipBenefitResourcesTx 只返回当前最终来源真正可用的资源。trial 必须同时满足 +// trial_enabled,且金币返现即使被历史脏配置绑定资源也会硬排除;paid 则读取全部 active benefit。 +// 先走配置覆盖索引,再按 resource 主键加锁,保证 admin 改绑和发放处于同一配置版本边界。 +func (r *Repository) listActiveVipBenefitResourcesTx(ctx context.Context, tx *sql.Tx, level int32, effectiveSource string) ([]activeVipBenefitResource, error) { + if level <= 0 || (effectiveSource != ledger.VipEffectiveSourcePaid && effectiveSource != ledger.VipEffectiveSourceTrial) { + return nil, nil + } + trialOnly := effectiveSource == ledger.VipEffectiveSourceTrial + rows, err := tx.QueryContext(ctx, ` + SELECT MIN(b.benefit_code), MAX(b.auto_equip), b.resource_id + FROM vip_level_benefits AS b + JOIN resources AS resource ON resource.app_code = b.app_code + AND resource.resource_id = b.resource_id + AND resource.status = 'active' + AND resource.resource_type = b.resource_type + WHERE b.app_code = ? AND b.level = ? AND b.status = 'active' AND b.resource_id > 0 + AND (? = FALSE OR (b.trial_enabled = TRUE AND b.benefit_code <> ?)) + GROUP BY b.resource_id + ORDER BY b.resource_id ASC`, appcode.FromContext(ctx), level, trialOnly, ledger.VipBenefitCodeDailyCoinRebate) + if err != nil { + return nil, err + } + type binding struct { + benefitCode string + autoEquip bool + resourceID int64 + } + bindings := make([]binding, 0, 4) + for rows.Next() { + var item binding + if err := rows.Scan(&item.benefitCode, &item.autoEquip, &item.resourceID); err != nil { + _ = rows.Close() + return nil, err + } + bindings = append(bindings, item) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return nil, err + } + if err := rows.Close(); err != nil { + return nil, err + } + + items := make([]activeVipBenefitResource, 0, len(bindings)) + seen := make(map[int64]struct{}, len(bindings)) + for _, binding := range bindings { + if _, exists := seen[binding.resourceID]; exists { + continue + } + resource, err := r.getResourceForUpdate(ctx, tx, binding.resourceID) + if err != nil { + return nil, err + } + seen[binding.resourceID] = struct{}{} + items = append(items, activeVipBenefitResource{ + BenefitCode: binding.benefitCode, + AutoEquip: binding.autoEquip, + Resource: resource, + }) + } + return items, nil +} + +// grantVipTrialBenefitResourcesTx 按 config_version 为一张体验卡创建来源独立的装扮 entitlement。 +// 改绑会生成新版本 grant;未变化资源复用同一卡已有 entitlement,既拿到新资源又不会重复旧资源。 +func (r *Repository) grantVipTrialBenefitResourcesTx(ctx context.Context, tx *sql.Tx, card ledger.VipTrialCard, configVersion int64, nowMS int64) error { + items, err := r.listActiveVipBenefitResourcesTx(ctx, tx, card.Level, ledger.VipEffectiveSourceTrial) + if err != nil || len(items) == 0 { + return err + } + resourceIDs := make([]int64, 0, len(items)) + for _, item := range items { + resourceIDs = append(resourceIDs, item.Resource.ResourceID) + } + sort.Slice(resourceIDs, func(left, right int) bool { return resourceIDs[left] < resourceIDs[right] }) + snapshot := struct { + TrialCardID string `json:"trial_card_id"` + Level int32 `json:"level"` + ExpiresAtMS int64 `json:"expires_at_ms"` + ResourceIDs []int64 `json:"resource_ids"` + }{TrialCardID: card.TrialCardID, Level: card.Level, ExpiresAtMS: card.ExpiresAtMS, ResourceIDs: resourceIDs} + snapshotJSON, err := json.Marshal(snapshot) + if err != nil { + return err + } + grantCommandID := "vip_trial_benefit:" + stableHash(fmt.Sprintf("%s|%s|%d", appcode.FromContext(ctx), card.TrialCardID, configVersion)) + requestHash := stableHash(fmt.Sprintf("%s|%d|%s", appcode.FromContext(ctx), card.UserID, snapshotJSON)) + if _, exists, err := r.lookupResourceGrant(ctx, tx, grantCommandID, requestHash); err != nil || exists { + return err + } + grantID := resourceGrantID(appcode.FromContext(ctx), grantCommandID) + originSource := strings.TrimSpace(card.GrantSource) + if originSource == "" { + originSource = ledger.VipGrantSourceActivity + } + subjectID := vipTrialBenefitGrantSubjectID(appcode.FromContext(ctx), card.TrialCardID) + if err := r.insertResourceGrant(ctx, tx, grantID, grantCommandID, card.UserID, resourcedomain.GrantSourceVIPTrial, + resourcedomain.GrantSubjectGroup, subjectID, requestHash, + string(snapshotJSON), "vip trial effective benefit resources", card.UserID, nowMS); err != nil { + return err + } + durationMS := card.ExpiresAtMS - nowMS + if durationMS <= 0 { + return xerr.New(xerr.VIPTrialCardExpired, "vip trial card expired") + } + for _, item := range items { + if entitlementID, exists, err := r.findExistingVipBenefitEntitlementTx(ctx, tx, card.UserID, + item.Resource.ResourceID, resourcedomain.GrantSourceVIPTrial, subjectID, + "vip_trial_benefit:"+card.TrialCardID, nowMS); err != nil { + return err + } else if exists { + if err := r.alignEntitlementExpiresAt(ctx, tx, entitlementID, card.ExpiresAtMS, nowMS); err != nil { + return err + } + continue + } + inserted, err := r.applyVipGrantItemWithoutAutoEquipTx(ctx, tx, grantID, grantCommandID, + card.UserID, item.Resource, 1, durationMS, nowMS) + if err != nil { + return err + } + if err := r.alignEntitlementExpiresAt(ctx, tx, inserted.EntitlementID, card.ExpiresAtMS, nowMS); err != nil { + return err + } + } + return r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ + resourceOutboxEvent("ResourceGroupGranted", grantCommandID, card.UserID, 0, map[string]any{ + "grant_id": grantID, "source": resourcedomain.GrantSourceVIPTrial, + "origin_source": originSource, "vip_level": card.Level, "config_version": configVersion, + "trial_card_id": card.TrialCardID, "expires_at_ms": card.ExpiresAtMS, + "resource_ids": resourceIDs, + }, nowMS), + }) +} + +// grantPaidVipBenefitResourcesTx 直接从当前 active benefit bindings 发付费 entitlement,彻底 +// 去掉“运营还要同步 reward group”的隐含前置条件。grant subject 绑定本次会员有效窗;配置改绑 +// 只补新资源,未变化资源复用同一有效窗已有 entitlement。 +func (r *Repository) grantPaidVipBenefitResourcesTx(ctx context.Context, tx *sql.Tx, vip ledger.UserVip, nowMS int64) ([]ledger.VipRewardItem, string, error) { + if !vip.Active || vip.Level <= 0 || vip.ExpiresAtMS <= nowMS { + return nil, "", nil + } + items, err := r.listActiveVipBenefitResourcesTx(ctx, tx, vip.Level, ledger.VipEffectiveSourcePaid) + if err != nil || len(items) == 0 { + return nil, "", err + } + resourceIDs := make([]int64, 0, len(items)) + for _, item := range items { + resourceIDs = append(resourceIDs, item.Resource.ResourceID) + } + sort.Slice(resourceIDs, func(left, right int) bool { return resourceIDs[left] < resourceIDs[right] }) + snapshot := struct { + UserID int64 `json:"user_id"` + Level int32 `json:"level"` + ExpiresAtMS int64 `json:"expires_at_ms"` + ConfigVersion int64 `json:"config_version"` + ResourceIDs []int64 `json:"resource_ids"` + }{vip.UserID, vip.Level, vip.ExpiresAtMS, vip.ConfigVersion, resourceIDs} + snapshotJSON, err := json.Marshal(snapshot) + if err != nil { + return nil, "", err + } + grantCommandID := "vip_paid_benefit:" + stableHash(fmt.Sprintf("%s|%d|%d|%d|%d", appcode.FromContext(ctx), vip.UserID, vip.Level, vip.ExpiresAtMS, vip.ConfigVersion)) + requestHash := stableHash(fmt.Sprintf("%s|%d|%s", appcode.FromContext(ctx), vip.UserID, snapshotJSON)) + if existingGrantID, exists, err := r.lookupResourceGrant(ctx, tx, grantCommandID, requestHash); err != nil { + return nil, "", err + } else if exists { + rewards, replayErr := r.rewardItemsByGrant(ctx, tx, existingGrantID, vip.ExpiresAtMS) + return rewards, existingGrantID, replayErr + } + grantID := resourceGrantID(appcode.FromContext(ctx), grantCommandID) + subjectID := "vip_paid_membership:" + stableHash(fmt.Sprintf("%s|%d|%d|%d", appcode.FromContext(ctx), vip.UserID, vip.Level, vip.ExpiresAtMS)) + if err := r.insertResourceGrant(ctx, tx, grantID, grantCommandID, vip.UserID, resourcedomain.GrantSourceVIPPaid, + resourcedomain.GrantSubjectGroup, subjectID, requestHash, string(snapshotJSON), + "paid vip effective benefit resources", vip.UserID, nowMS); err != nil { + return nil, "", err + } + durationMS := vip.ExpiresAtMS - nowMS + rewards := make([]ledger.VipRewardItem, 0, len(items)) + for _, item := range items { + if entitlementID, exists, err := r.findExistingVipBenefitEntitlementTx(ctx, tx, vip.UserID, + item.Resource.ResourceID, resourcedomain.GrantSourceVIPPaid, subjectID, "", nowMS); err != nil { + return nil, "", err + } else if exists { + if err := r.alignEntitlementExpiresAt(ctx, tx, entitlementID, vip.ExpiresAtMS, nowMS); err != nil { + return nil, "", err + } + } else { + inserted, err := r.applyVipGrantItemWithoutAutoEquipTx(ctx, tx, grantID, grantCommandID, + vip.UserID, item.Resource, 1, durationMS, nowMS) + if err != nil { + return nil, "", err + } + if err := r.alignEntitlementExpiresAt(ctx, tx, inserted.EntitlementID, vip.ExpiresAtMS, nowMS); err != nil { + return nil, "", err + } + } + rewards = append(rewards, vipRewardItemFromBenefitResource(item.Resource, vip.ExpiresAtMS)) + } + if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ + resourceOutboxEvent("ResourceGroupGranted", grantCommandID, vip.UserID, 0, map[string]any{ + "grant_id": grantID, "source": resourcedomain.GrantSourceVIPPaid, + "vip_level": vip.Level, "config_version": vip.ConfigVersion, + "expires_at_ms": vip.ExpiresAtMS, "resource_ids": resourceIDs, + }, nowMS), + }); err != nil { + return nil, "", err + } + return rewards, grantID, nil +} + +func vipRewardItemFromBenefitResource(resource resourcedomain.Resource, expiresAtMS int64) ledger.VipRewardItem { + return ledger.VipRewardItem{ + ResourceID: resource.ResourceID, ResourceCode: resource.ResourceCode, + ResourceType: resource.ResourceType, Name: resource.Name, Quantity: 1, + ExpiresAtMS: expiresAtMS, AssetURL: resource.AssetURL, + PreviewURL: resource.PreviewURL, AnimationURL: resource.AnimationURL, + } +} + +func vipTrialBenefitGrantSubjectID(appCode string, trialCardID string) string { + return "vip_trial_card:" + stableHash(appcode.Normalize(appCode)+"|"+strings.TrimSpace(trialCardID)) +} + +// findExistingVipBenefitEntitlementTx 只在同一 VIP 来源和同一业务 subject 内复用资源, +// paid/trial 永不互相合并。legacyTrialCommand 只兼容升级前已经由同一体验卡创建的固定命令。 +func (r *Repository) findExistingVipBenefitEntitlementTx(ctx context.Context, tx *sql.Tx, userID int64, resourceID int64, grantSource string, subjectID string, legacyTrialCommand string, nowMS int64) (string, bool, error) { + row := tx.QueryRowContext(ctx, ` + SELECT e.entitlement_id + FROM user_resource_entitlements AS e + JOIN resource_grants AS g ON g.app_code = e.app_code AND g.grant_id = e.source_grant_id + WHERE e.app_code = ? AND e.user_id = ? AND e.resource_id = ? + AND e.status = 'active' AND e.effective_at_ms <= ? + AND (e.expires_at_ms = 0 OR e.expires_at_ms > ?) AND e.remaining_quantity > 0 + AND g.status = ? + AND ((g.grant_source = ? AND g.grant_subject_id = ?) + OR (? <> '' AND g.command_id = ?)) + ORDER BY e.expires_at_ms DESC, e.created_at_ms DESC + LIMIT 1 FOR UPDATE`, + appcode.FromContext(ctx), userID, resourceID, nowMS, nowMS, + resourcedomain.GrantStatusDone, grantSource, subjectID, legacyTrialCommand, legacyTrialCommand, + ) + var entitlementID string + if err := row.Scan(&entitlementID); errors.Is(err, sql.ErrNoRows) { + return "", false, nil + } else if err != nil { + return "", false, err + } + return entitlementID, true, nil +} + +// reconcileTieredVipResourceEquipmentTx 删除不属于最终 effective source+level 的全部历史 VIP 装备, +// 再只按当前 benefit.auto_equip 恢复对应来源的 entitlement。paid/trial 即使绑定同一 resource_id, +// 也会切换到各自 source grant,避免任一来源到期影响另一来源。 +func (r *Repository) reconcileTieredVipResourceEquipmentTx(ctx context.Context, tx *sql.Tx, userID int64, level int32, effectiveSource string, commandID string, nowMS int64) error { + trialOnly := effectiveSource == ledger.VipEffectiveSourceTrial + if effectiveSource != ledger.VipEffectiveSourcePaid && effectiveSource != ledger.VipEffectiveSourceTrial { + level = 0 + effectiveSource = ledger.VipEffectiveSourceNone + } + if strings.TrimSpace(commandID) == "" { + commandID = "vip_reconcile:" + stableHash(fmt.Sprintf("%s|%d|%d|%s", appcode.FromContext(ctx), userID, level, effectiveSource)) + } + result, err := tx.ExecContext(ctx, ` + DELETE equipment + FROM user_resource_equipment AS equipment + WHERE equipment.app_code = ? AND equipment.user_id = ? + AND ( + EXISTS ( + SELECT 1 + FROM vip_level_benefits AS mapped + JOIN vip_program_configs AS mapped_program ON mapped_program.app_code = mapped.app_code + AND mapped_program.program_type = ? + WHERE mapped.app_code = equipment.app_code + AND mapped.resource_id = equipment.resource_id + ) + OR EXISTS ( + SELECT 1 FROM vip_benefit_resource_binding_history AS historical + WHERE historical.app_code = equipment.app_code + AND historical.resource_id = equipment.resource_id + AND historical.program_type = ? + ) + ) + AND NOT EXISTS ( + SELECT 1 + FROM vip_level_benefits AS current_benefit + JOIN resources AS current_resource ON current_resource.app_code = current_benefit.app_code + AND current_resource.resource_id = current_benefit.resource_id + AND current_resource.status = 'active' + AND current_resource.resource_type = current_benefit.resource_type + WHERE current_benefit.app_code = equipment.app_code + AND current_benefit.level = ? + AND current_benefit.status = 'active' + AND current_benefit.resource_id = equipment.resource_id + AND (? = FALSE OR (current_benefit.trial_enabled = TRUE AND current_benefit.benefit_code <> ?)) + )`, appcode.FromContext(ctx), userID, + ledger.VipProgramTypeTieredPrivilegeV1, ledger.VipProgramTypeTieredPrivilegeV1, + level, trialOnly, ledger.VipBenefitCodeDailyCoinRebate) + if err != nil { + return err + } + removed, err := result.RowsAffected() + if err != nil { + return err + } + items, err := r.listActiveVipBenefitResourcesTx(ctx, tx, level, effectiveSource) + if err != nil { + return err + } + events := make([]walletOutboxEvent, 0, len(items)+1) + if removed > 0 { + events = append(events, resourceOutboxEvent("UserResourceChanged", boundedDerivedCommandID("vip_reconcile_remove:", commandID), userID, 0, map[string]any{ + "action": "vip_reconcile", "vip_level": level, "effective_source": effectiveSource, + "removed_count": removed, "updated_at_ms": nowMS, + }, nowMS)) + } + for _, item := range items { + if !item.AutoEquip || !isEquipableResourceType(item.Resource.ResourceType) { + continue + } + entitlement, err := r.queryVipBenefitEntitlementForUpdateTx(ctx, tx, userID, item.Resource.ResourceID, effectiveSource, nowMS) + if xerr.IsCode(err, xerr.NotFound) { + // 调用方会先做 paid/trial 懒补齐;仍缺失说明目录或历史数据不完整,失败关闭展示但不阻塞状态读取。 + continue + } + if err != nil { + return err + } + alreadyEquipped, err := r.isUserResourceEntitlementEquippedTx(ctx, tx, userID, entitlement) + if err != nil { + return err + } + if alreadyEquipped { + continue + } + if err := r.equipUserResourceEntitlementTx(ctx, tx, userID, entitlement, nowMS); err != nil { + return err + } + events = append(events, resourceOutboxEvent("UserResourceChanged", boundedDerivedCommandID("vip_reconcile_equip:", commandID), userID, item.Resource.ResourceID, map[string]any{ + "action": "equip", "vip_level": level, "effective_source": effectiveSource, + "benefit_code": item.BenefitCode, + "resource_id": item.Resource.ResourceID, "resource_type": item.Resource.ResourceType, + "entitlement_id": entitlement.EntitlementID, "updated_at_ms": nowMS, + }, nowMS)) + } + if len(events) == 0 { + return nil + } + return r.insertWalletOutbox(ctx, tx, events) +} + +func (r *Repository) isUserResourceEntitlementEquippedTx(ctx context.Context, tx *sql.Tx, userID int64, entitlement resourcedomain.UserResourceEntitlement) (bool, error) { + var equipped bool + err := tx.QueryRowContext(ctx, ` + SELECT EXISTS( + SELECT 1 FROM user_resource_equipment + WHERE app_code = ? AND user_id = ? AND resource_type = ? AND entitlement_id = ? + )`, appcode.FromContext(ctx), userID, + resourcedomain.NormalizeResourceType(entitlement.Resource.ResourceType), entitlement.EntitlementID, + ).Scan(&equipped) + return equipped, err +} + +// queryVipBenefitEntitlementForUpdateTx 强制 reconcile 选中最终来源自己的 entitlement。 +// trial 还绑定当前佩戴 card subject,避免两张同等级体验卡共享剩余时长不同的资源实例。 +func (r *Repository) queryVipBenefitEntitlementForUpdateTx(ctx context.Context, tx *sql.Tx, userID int64, resourceID int64, effectiveSource string, nowMS int64) (resourcedomain.UserResourceEntitlement, error) { + grantWhere := `g.grant_source = ?` + args := []any{appcode.FromContext(ctx), userID, resourceID, nowMS, nowMS} + if effectiveSource == ledger.VipEffectiveSourcePaid { + args = append(args, resourcedomain.GrantSourceVIPPaid) + } else if effectiveSource == ledger.VipEffectiveSourceTrial { + var trialCardID string + if err := tx.QueryRowContext(ctx, ` + SELECT card.trial_card_id + FROM user_resource_equipment AS equipment + JOIN user_vip_trial_cards AS card ON card.app_code = equipment.app_code + AND card.user_id = equipment.user_id AND card.entitlement_id = equipment.entitlement_id + WHERE equipment.app_code = ? AND equipment.user_id = ? AND equipment.resource_type = ? + LIMIT 1`, appcode.FromContext(ctx), userID, resourcedomain.TypeVIPTrialCard).Scan(&trialCardID); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "equipped vip trial card not found") + } + return resourcedomain.UserResourceEntitlement{}, err + } + grantWhere = `((g.grant_source = ? AND g.grant_subject_id = ?) OR g.command_id = ?)` + args = append(args, resourcedomain.GrantSourceVIPTrial, + vipTrialBenefitGrantSubjectID(appcode.FromContext(ctx), trialCardID), "vip_trial_benefit:"+trialCardID) + } else { + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "effective vip resource entitlement not found") + } + query := userResourceSelectSQL() + ` + JOIN resource_grants AS g ON g.app_code = e.app_code AND g.grant_id = e.source_grant_id + WHERE e.app_code = ? AND e.user_id = ? AND e.resource_id = ? + AND e.status = 'active' AND e.effective_at_ms <= ? + AND (e.expires_at_ms = 0 OR e.expires_at_ms > ?) AND e.remaining_quantity > 0 + AND g.status = 'succeeded' AND ` + grantWhere + ` + ORDER BY e.expires_at_ms DESC, e.created_at_ms DESC + LIMIT 1 FOR UPDATE` + item, err := scanUserResourceEntitlement(tx.QueryRowContext(ctx, query, args...)) + if errors.Is(err, sql.ErrNoRows) { + return resourcedomain.UserResourceEntitlement{}, xerr.New(xerr.NotFound, "effective vip resource entitlement not found") + } + return item, err +} + +// isTieredVipBenefitResourceTx 识别当前或历史任一 tiered VIP 绑定;program 状态不会参与, +// 因为关闭 program 时旧 entitlement 也必须失败关闭,而不是变成普通可装备资源。 +func (r *Repository) isTieredVipBenefitResourceTx(ctx context.Context, tx *sql.Tx, resourceID int64) (bool, error) { + var bound bool + err := tx.QueryRowContext(ctx, ` + SELECT ( + EXISTS( + SELECT 1 FROM vip_level_benefits AS benefit + JOIN vip_program_configs AS program ON program.app_code = benefit.app_code + AND program.program_type = ? + WHERE benefit.app_code = ? AND benefit.resource_id = ? + ) + OR EXISTS( + SELECT 1 FROM vip_benefit_resource_binding_history AS history + WHERE history.app_code = ? AND history.resource_id = ? AND history.program_type = ? + ) + )`, ledger.VipProgramTypeTieredPrivilegeV1, appcode.FromContext(ctx), resourceID, + appcode.FromContext(ctx), resourceID, ledger.VipProgramTypeTieredPrivilegeV1, + ).Scan(&bound) + return bound, err +} + +// requireCurrentVipResourceBindingForUserTx 是装备提交事务内的最后门禁。它先共享锁 program, +// 因而 admin 的 DELETE+INSERT 改绑不可能夹在校验与 equipment 写入之间。 +func (r *Repository) requireCurrentVipResourceBindingForUserTx(ctx context.Context, tx *sql.Tx, userID int64, resourceID int64, nowMS int64) error { + bound, err := r.isTieredVipBenefitResourceTx(ctx, tx, resourceID) + if err != nil || !bound { + return err + } + requirement, err := r.firstTieredVipBenefitRequirementTx(ctx, tx, resourceID) + if err != nil { + return err + } + program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, appcode.FromContext(ctx))) + if errors.Is(err, sql.ErrNoRows) { + return vipBenefitRequiredForResourceEquip(requirement, resourceID, 0) + } + if err != nil { + return err + } + if program.ProgramType != ledger.VipProgramTypeTieredPrivilegeV1 || program.Status != ledger.VipStatusActive { + return vipBenefitRequiredForResourceEquip(requirement, resourceID, 0) + } + state, err := r.getVipStateFromSnapshot(ctx, tx, userID, nowMS, program) + if err != nil { + return err + } + for _, benefit := range state.EffectiveBenefits { + if benefit.ResourceID == resourceID && benefit.Resource != nil && benefit.Resource.ResourceID == resourceID { + return nil + } + } + return vipBenefitRequiredForResourceEquip(requirement, resourceID, state.EffectiveVip.Level) +} + +func (r *Repository) firstTieredVipBenefitRequirementTx(ctx context.Context, tx *sql.Tx, resourceID int64) (resourcedomain.VipBenefitRequirement, error) { + var requirement resourcedomain.VipBenefitRequirement + err := tx.QueryRowContext(ctx, ` + SELECT binding.benefit_code, MIN(binding.level), binding.resource_type + FROM ( + SELECT benefit.benefit_code, benefit.level, benefit.resource_type + FROM vip_level_benefits AS benefit + JOIN vip_program_configs AS program ON program.app_code = benefit.app_code + AND program.program_type = ? + WHERE benefit.app_code = ? AND benefit.resource_id = ? + UNION ALL + SELECT history.benefit_code, history.level, history.resource_type + FROM vip_benefit_resource_binding_history AS history + WHERE history.app_code = ? AND history.resource_id = ? AND history.program_type = ? + ) AS binding + GROUP BY binding.benefit_code, binding.resource_type + ORDER BY MIN(binding.level) ASC, binding.benefit_code ASC + LIMIT 1`, ledger.VipProgramTypeTieredPrivilegeV1, appcode.FromContext(ctx), resourceID, + appcode.FromContext(ctx), resourceID, ledger.VipProgramTypeTieredPrivilegeV1, + ).Scan(&requirement.BenefitCode, &requirement.RequiredLevel, &requirement.ResourceType) + if errors.Is(err, sql.ErrNoRows) { + return resourcedomain.VipBenefitRequirement{}, xerr.New(xerr.Internal, "vip resource binding history is missing") + } + return requirement, err +} + +func vipBenefitRequiredForResourceEquip(requirement resourcedomain.VipBenefitRequirement, resourceID int64, currentLevel int32) error { + return xerr.NewWithMetadata(xerr.VIPBenefitRequired, "vip benefit is required for resource equip", map[string]string{ + "benefit_code": requirement.BenefitCode, + "required_level": fmt.Sprintf("%d", requirement.RequiredLevel), + "current_effective_level": fmt.Sprintf("%d", currentLevel), + "resource_id": fmt.Sprintf("%d", resourceID), + "action": "equip_resource", + }) +} + +// ensureTieredVipResourceState 是状态读取时的安全懒补齐:老会员无需全表迁移,首次读取时会 +// 在单用户事务内补发当前 paid/trial resources,并按最终来源恢复 auto_equip。自然过期的 trial +// 装备指针在调用前已清理,因此这里会自动回落仍有效 paid。 +func (r *Repository) ensureTieredVipResourceState(ctx context.Context, userID int64, nowMS int64) error { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, appcode.FromContext(ctx))) + if errors.Is(err, sql.ErrNoRows) { + return nil + } + if err != nil { + return err + } + if program.ProgramType != ledger.VipProgramTypeTieredPrivilegeV1 { + return tx.Commit() + } + // 与购买保持 program -> membership 的统一锁序;无 membership 行时只读取 trial/none。 + var lockedUserID int64 + lockErr := tx.QueryRowContext(ctx, ` + SELECT user_id FROM user_vip_memberships + WHERE app_code = ? AND user_id = ? FOR UPDATE`, appcode.FromContext(ctx), userID).Scan(&lockedUserID) + if lockErr != nil && !errors.Is(lockErr, sql.ErrNoRows) { + return lockErr + } + state, err := r.getVipStateFromSnapshot(ctx, tx, userID, nowMS, program) + if err != nil { + return err + } + if program.Status == ledger.VipStatusActive && state.PaidVip.Active { + paid := state.PaidVip + paid.ProgramType = program.ProgramType + paid.ConfigVersion = program.ConfigVersion + if _, _, err := r.grantPaidVipBenefitResourcesTx(ctx, tx, paid, nowMS); err != nil { + return err + } + } + if program.Status == ledger.VipStatusActive && state.EffectiveSource == ledger.VipEffectiveSourceTrial && state.EquippedTrialCard != nil { + if err := r.grantVipTrialBenefitResourcesTx(ctx, tx, *state.EquippedTrialCard, program.ConfigVersion, nowMS); err != nil { + return err + } + } + effectiveLevel := int32(0) + effectiveSource := ledger.VipEffectiveSourceNone + if state.EffectiveVip.Active { + effectiveLevel = state.EffectiveVip.Level + effectiveSource = state.EffectiveSource + } + commandID := "vip_state_sync:" + stableHash(fmt.Sprintf("%s|%d|%d|%s|%d", appcode.FromContext(ctx), userID, effectiveLevel, effectiveSource, program.ConfigVersion)) + if err := r.reconcileTieredVipResourceEquipmentTx(ctx, tx, userID, effectiveLevel, effectiveSource, commandID, nowMS); err != nil { + return err + } + return tx.Commit() +} + +type vipResourceBindingKey struct { + Level int32 + ResourceID int64 +} + +type currentVipResourceBinding struct { + ResourceType string + PaidAllowed bool + TrialAllowed bool +} + +type vipBatchEffectiveLevels struct { + Paid int32 + Trial int32 +} + +// filterCurrentVipEquipmentBatch 对整个 appearance 批次只执行三次配置/用户事实查询: +// 1) 共享锁 program;2) 一次 current+history resource mapping;3) 一次 paid+trial effective level。 +// 查询数量不随 user 数增长,且所有条件都以 app_code 开头,避免高频房间列表退化成 N+1 或全表扫描。 +func (r *Repository) filterCurrentVipEquipmentBatch(ctx context.Context, userIDs []int64, grouped map[int64][]resourcedomain.UserResourceEntitlement, nowMS int64) error { + resourceIDSet := make(map[int64]struct{}, 8) + for _, resources := range grouped { + for _, entitlement := range resources { + if entitlement.ResourceID > 0 { + resourceIDSet[entitlement.ResourceID] = struct{}{} + } + } + } + if len(resourceIDSet) == 0 { + return nil + } + resourceIDs := make([]int64, 0, len(resourceIDSet)) + for resourceID := range resourceIDSet { + resourceIDs = append(resourceIDs, resourceID) + } + sort.Slice(resourceIDs, func(left, right int) bool { return resourceIDs[left] < resourceIDs[right] }) + + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer func() { _ = tx.Rollback() }() + program, programErr := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, appcode.FromContext(ctx))) + if programErr != nil && !errors.Is(programErr, sql.ErrNoRows) { + return programErr + } + + mappingArgs := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(resourceIDs)...) + mappingArgs = append(mappingArgs, ledger.VipProgramTypeTieredPrivilegeV1, + ledger.VipProgramTypeTieredPrivilegeV1, appcode.FromContext(ctx)) + mappingArgs = append(mappingArgs, int64AnyArgs(resourceIDs)...) + rows, err := tx.QueryContext(ctx, ` + SELECT history.resource_id, 0 AS level, '' AS resource_type, + FALSE AS trial_enabled, FALSE AS current_allowed + FROM vip_benefit_resource_binding_history AS history + WHERE history.app_code = ? AND history.resource_id IN (`+placeholders(len(resourceIDs))+`) + AND history.program_type = ? + UNION ALL + SELECT benefit.resource_id, benefit.level, benefit.resource_type, + (benefit.trial_enabled AND benefit.benefit_code <> 'daily_coin_rebate') AS trial_enabled, + (benefit.status = 'active' AND resource.resource_id IS NOT NULL) AS current_allowed + FROM vip_level_benefits AS benefit + JOIN vip_program_configs AS current_program ON current_program.app_code = benefit.app_code + AND current_program.program_type = ? + LEFT JOIN resources AS resource ON resource.app_code = benefit.app_code + AND resource.resource_id = benefit.resource_id + AND resource.status = 'active' AND resource.resource_type = benefit.resource_type + WHERE benefit.app_code = ? AND benefit.resource_id IN (`+placeholders(len(resourceIDs))+`)`, mappingArgs..., + ) + if err != nil { + return err + } + historical := make(map[int64]struct{}, len(resourceIDs)) + bindings := make(map[vipResourceBindingKey]currentVipResourceBinding, len(resourceIDs)) + for rows.Next() { + var resourceID int64 + var level int32 + var resourceType string + var trialEnabled bool + var currentAllowed bool + if err := rows.Scan(&resourceID, &level, &resourceType, &trialEnabled, ¤tAllowed); err != nil { + _ = rows.Close() + return err + } + historical[resourceID] = struct{}{} + if !currentAllowed || level <= 0 { + continue + } + key := vipResourceBindingKey{Level: level, ResourceID: resourceID} + binding := bindings[key] + binding.ResourceType = resourcedomain.NormalizeResourceType(resourceType) + binding.PaidAllowed = true + binding.TrialAllowed = binding.TrialAllowed || trialEnabled + bindings[key] = binding + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return err + } + if err := rows.Close(); err != nil { + return err + } + if len(historical) == 0 { + return tx.Commit() + } + if programErr != nil || program.ProgramType != ledger.VipProgramTypeTieredPrivilegeV1 || program.Status != ledger.VipStatusActive { + filterVipEquipmentByAccess(grouped, historical, bindings, nil, false) + return tx.Commit() + } + + accessArgs := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(userIDs)...) + accessArgs = append(accessArgs, nowMS, appcode.FromContext(ctx)) + accessArgs = append(accessArgs, int64AnyArgs(userIDs)...) + accessArgs = append(accessArgs, resourcedomain.TypeVIPTrialCard, ledger.VipTrialCardStatusActive, + nowMS, nowMS, nowMS, nowMS) + accessRows, err := tx.QueryContext(ctx, ` + SELECT membership.user_id, membership.level, 'paid' AS effective_source + FROM user_vip_memberships AS membership FORCE INDEX (PRIMARY) + WHERE membership.app_code = ? AND membership.user_id IN (`+placeholders(len(userIDs))+`) + AND membership.level > 0 AND membership.expires_at_ms > ? + UNION ALL + SELECT STRAIGHT_JOIN card.user_id, card.level, 'trial' AS effective_source + FROM user_resource_equipment AS equipment FORCE INDEX (PRIMARY) + JOIN user_vip_trial_cards AS card FORCE INDEX (uk_vip_trial_card_entitlement) ON card.app_code = equipment.app_code + AND card.user_id = equipment.user_id AND card.entitlement_id = equipment.entitlement_id + JOIN user_resource_entitlements AS entitlement ON entitlement.app_code = equipment.app_code + AND entitlement.user_id = equipment.user_id AND entitlement.entitlement_id = equipment.entitlement_id + JOIN resources AS resource ON resource.app_code = equipment.app_code AND resource.resource_id = equipment.resource_id + WHERE equipment.app_code = ? AND equipment.user_id IN (`+placeholders(len(userIDs))+`) + AND equipment.resource_type = ? AND card.status = ? + AND card.effective_at_ms <= ? AND card.expires_at_ms > ? + AND entitlement.status = 'active' AND entitlement.effective_at_ms <= ? + AND (entitlement.expires_at_ms = 0 OR entitlement.expires_at_ms > ?) + AND entitlement.remaining_quantity > 0 AND resource.status = 'active'`, accessArgs...) + if err != nil { + return err + } + levels := make(map[int64]vipBatchEffectiveLevels, len(userIDs)) + for accessRows.Next() { + var userID int64 + var level int32 + var source string + if err := accessRows.Scan(&userID, &level, &source); err != nil { + _ = accessRows.Close() + return err + } + access := levels[userID] + if source == ledger.VipEffectiveSourceTrial { + access.Trial = level + } else { + access.Paid = level + } + levels[userID] = access + } + if err := accessRows.Err(); err != nil { + _ = accessRows.Close() + return err + } + if err := accessRows.Close(); err != nil { + return err + } + filterVipEquipmentByAccess(grouped, historical, bindings, levels, program.TrialCardEnabled) + return tx.Commit() +} + +func filterVipEquipmentByAccess(grouped map[int64][]resourcedomain.UserResourceEntitlement, historical map[int64]struct{}, bindings map[vipResourceBindingKey]currentVipResourceBinding, levels map[int64]vipBatchEffectiveLevels, trialCardEnabled bool) { + for userID, resources := range grouped { + filtered := resources[:0] + access := levels[userID] + effectiveLevel := access.Paid + effectiveSource := ledger.VipEffectiveSourcePaid + if trialCardEnabled && access.Trial > 0 { + effectiveLevel = access.Trial + effectiveSource = ledger.VipEffectiveSourceTrial + } + for _, entitlement := range resources { + if _, vipBound := historical[entitlement.ResourceID]; !vipBound { + filtered = append(filtered, entitlement) + continue + } + binding, exact := bindings[vipResourceBindingKey{Level: effectiveLevel, ResourceID: entitlement.ResourceID}] + if !exact || binding.ResourceType != resourcedomain.NormalizeResourceType(entitlement.Resource.ResourceType) { + continue + } + if effectiveSource == ledger.VipEffectiveSourceTrial && !binding.TrialAllowed { + continue + } + if effectiveSource == ledger.VipEffectiveSourcePaid && !binding.PaidAllowed { + continue + } + filtered = append(filtered, entitlement) + } + grouped[userID] = filtered + } +} diff --git a/services/wallet-service/internal/storage/mysql/vip_grant_repository.go b/services/wallet-service/internal/storage/mysql/vip_grant_repository.go index 6fa3377d..cae66d5e 100644 --- a/services/wallet-service/internal/storage/mysql/vip_grant_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_grant_repository.go @@ -90,7 +90,7 @@ func (r *Repository) GrantVip(ctx context.Context, command ledger.GrantVipComman } transactionID := transactionID(command.AppCode, command.CommandID) recordID := "vip_grant_" + stableHash(command.AppCode+"|"+command.CommandID) - expectedResourceGrantID := resourceGrantID(command.AppCode, "vip_reward:"+command.CommandID) + expectedResourceGrantID := resourceGrantID(command.AppCode, boundedDerivedCommandID("vip_reward:", command.CommandID)) metadata := map[string]any{ "app_code": command.AppCode, "grant_id": recordID, diff --git a/services/wallet-service/internal/storage/mysql/vip_program_repository.go b/services/wallet-service/internal/storage/mysql/vip_program_repository.go index 2598d73c..82919675 100644 --- a/services/wallet-service/internal/storage/mysql/vip_program_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_program_repository.go @@ -64,6 +64,9 @@ func (r *Repository) GetVipState(ctx context.Context, userID int64) (ledger.VipS if err := r.pruneExpiredUserResourceEquipment(ctx, []int64{userID}, []string{resourcedomain.TypeVIPTrialCard}, nowMS); err != nil { return ledger.VipState{}, err } + if err := r.ensureTieredVipResourceState(ctx, userID, nowMS); err != nil { + return ledger.VipState{}, err + } tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelRepeatableRead}) if err != nil { return ledger.VipState{}, err @@ -169,10 +172,23 @@ func (r *Repository) CheckVipBenefit(ctx context.Context, userID int64, benefitC return ledger.CheckVipBenefitResult{}, err } code := strings.ToLower(strings.TrimSpace(benefitCode)) + var requiredLevel int32 + // idx_vip_benefit_code(app_code, benefit_code, status, level) 让该 MIN 只扫描单 App、 + // 单权益的 active 索引区间,不会随所有等级和权益目录增长退化成全表扫描。 + if err := r.db.QueryRowContext(ctx, ` + SELECT COALESCE(MIN(level), 0) + FROM vip_level_benefits + WHERE app_code = ? AND benefit_code = ? AND status = ?`, + appcode.FromContext(ctx), code, ledger.VipStatusActive, + ).Scan(&requiredLevel); err != nil { + return ledger.CheckVipBenefitResult{}, err + } result := ledger.CheckVipBenefitResult{ - State: state, - DenialReason: "benefit_not_enabled", - EvaluatedAtMS: state.EvaluatedAtMS, + State: state, + DenialReason: "benefit_not_enabled", + EvaluatedAtMS: state.EvaluatedAtMS, + RequiredLevel: requiredLevel, + CurrentEffectiveLevel: state.EffectiveVip.Level, } if !state.EffectiveVip.Active { result.DenialReason = "vip_inactive" @@ -233,17 +249,22 @@ type vipBenefitQuerier interface { func (r *Repository) listVipLevelBenefits(ctx context.Context, querier vipBenefitQuerier, level int32, activeOnly bool) ([]ledger.VipBenefit, error) { query := ` - SELECT benefit_code, name, benefit_type, unlock_level, status, trial_enabled, - resource_id, resource_type, execution_scope, auto_equip, sort_order, - metadata_json, created_at_ms, updated_at_ms - FROM vip_level_benefits - WHERE app_code = ? AND level = ?` + SELECT b.benefit_code, b.name, b.benefit_type, b.unlock_level, b.status, b.trial_enabled, + b.resource_id, b.resource_type, b.execution_scope, b.auto_equip, b.sort_order, + b.metadata_json, b.created_at_ms, b.updated_at_ms, + ` + nullableResourceColumnsWithAlias("r") + ` + FROM vip_level_benefits AS b + LEFT JOIN resources AS r ON r.app_code = b.app_code + AND r.resource_id = b.resource_id + AND r.status = 'active' + AND r.resource_type = b.resource_type + WHERE b.app_code = ? AND b.level = ?` args := []any{appcode.FromContext(ctx), level} if activeOnly { - query += ` AND status = ?` + query += ` AND b.status = ?` args = append(args, ledger.VipStatusActive) } - query += ` ORDER BY sort_order ASC, benefit_code ASC` + query += ` ORDER BY b.sort_order ASC, b.benefit_code ASC` rows, err := querier.QueryContext(ctx, query, args...) if err != nil { return nil, err @@ -263,6 +284,8 @@ func (r *Repository) listVipLevelBenefits(ctx context.Context, querier vipBenefi func scanVipBenefit(scanner vipProgramScanner) (ledger.VipBenefit, error) { var benefit ledger.VipBenefit var metadata sql.NullString + var resource resourcedomain.Resource + var resourceScopesJSON string err := scanner.Scan( &benefit.BenefitCode, &benefit.Name, @@ -278,6 +301,30 @@ func scanVipBenefit(scanner vipProgramScanner) (ledger.VipBenefit, error) { &metadata, &benefit.CreatedAtMS, &benefit.UpdatedAtMS, + &resource.AppCode, + &resource.ResourceID, + &resource.ResourceCode, + &resource.ResourceType, + &resource.Name, + &resource.Status, + &resource.Grantable, + &resource.ManagerGrantEnabled, + &resource.GrantStrategy, + &resource.WalletAssetType, + &resource.WalletAssetAmount, + &resource.PriceType, + &resource.CoinPrice, + &resource.GiftPointAmount, + &resourceScopesJSON, + &resource.AssetURL, + &resource.PreviewURL, + &resource.AnimationURL, + &resource.MetadataJSON, + &resource.SortOrder, + &resource.CreatedByUserID, + &resource.UpdatedByUserID, + &resource.CreatedAtMS, + &resource.UpdatedAtMS, ) if metadata.Valid { benefit.MetadataJSON = metadata.String @@ -287,9 +334,31 @@ func scanVipBenefit(scanner vipProgramScanner) (ledger.VipBenefit, error) { // 无需按 benefit_code 重复维护 JSON schema。 benefit.Presentation, err = ledger.ParseVipBenefitPresentation(benefit.MetadataJSON) } + if err == nil && resource.ResourceID > 0 { + resource.UsageScopes = parseStringArray(resourceScopesJSON) + benefit.Resource = vipBenefitResourceProjection(resource) + } return benefit, err } +func vipBenefitResourceProjection(resource resourcedomain.Resource) *ledger.VipBenefitResource { + if resource.ResourceID <= 0 { + return nil + } + return &ledger.VipBenefitResource{ + AppCode: resource.AppCode, ResourceID: resource.ResourceID, ResourceCode: resource.ResourceCode, + ResourceType: resource.ResourceType, Name: resource.Name, Status: resource.Status, + Grantable: resource.Grantable, ManagerGrantEnabled: resource.ManagerGrantEnabled, + GrantStrategy: resource.GrantStrategy, WalletAssetType: resource.WalletAssetType, + WalletAssetAmount: resource.WalletAssetAmount, PriceType: resource.PriceType, + CoinPrice: resource.CoinPrice, GiftPointAmount: resource.GiftPointAmount, + UsageScopes: resource.UsageScopes, AssetURL: resource.AssetURL, PreviewURL: resource.PreviewURL, + AnimationURL: resource.AnimationURL, MetadataJSON: resource.MetadataJSON, SortOrder: resource.SortOrder, + CreatedByUserID: resource.CreatedByUserID, UpdatedByUserID: resource.UpdatedByUserID, + CreatedAtMS: resource.CreatedAtMS, UpdatedAtMS: resource.UpdatedAtMS, + } +} + type vipRowQuerier interface { QueryRowContext(context.Context, string, ...any) *sql.Row } diff --git a/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go b/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go index eb5741d2..5f67a5bb 100644 --- a/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_purchase_repository.go @@ -85,6 +85,11 @@ func (r *Repository) PurchaseVip(ctx context.Context, command ledger.PurchaseVip } account, err := r.lockAccount(ctx, tx, command.UserID, ledger.AssetCoin, false, nowMs) if err != nil { + // 从未持有 COIN 的用户没有 wallet_account 行,lockAccount 会先返回通用余额不足; + // VIP 购买对 Flutter 的稳定契约必须与“已有账户但余额不足”一致,统一成专用错误码。 + if xerr.IsCode(err, xerr.InsufficientBalance) { + return ledger.PurchaseVipReceipt{}, xerr.New(xerr.VIPInsufficientCoin, "insufficient coin balance") + } return ledger.PurchaseVipReceipt{}, err } if account.AvailableAmount < level.PriceCoin { @@ -253,19 +258,15 @@ func (r *Repository) activateUserVip(ctx context.Context, tx *sql.Tx, command vi if err != nil { return vipActivationResult{}, err } - rewardItems, resourceGrantID, err := r.applyVipRewardGroup(ctx, tx, vipRewardGrantCommand{ - AppCode: command.AppCode, - CommandID: command.CommandID, - UserID: command.UserID, - Source: command.Source, - Reason: command.Reason, - OperatorUserID: command.OperatorUserID, - }, command.Level, expiresAt, command.NowMS) - if err != nil { - return vipActivationResult{}, err + activationVIP := ledger.UserVip{ + UserID: command.UserID, Level: command.Level.Level, Name: command.Level.Name, Active: true, + StartedAtMS: startedAt, ExpiresAtMS: expiresAt, UpdatedAtMS: command.NowMS, + ProgramType: command.Program.ProgramType, ConfigVersion: command.Program.ConfigVersion, } + // 先更新 paid membership,再计算 paid+trial 的最终来源。这样用户正佩戴体验卡时续费, + // paid entitlement 会延长但当前 appearance 仍保持 trial,而不会被错误切回 paid 等级。 if _, err := tx.ExecContext(ctx, ` - UPDATE user_vip_memberships + UPDATE user_vip_memberships SET level = ?, name = ?, status = ?, program_type = ?, config_version = ?, started_at_ms = ?, expires_at_ms = ?, updated_at_ms = ? WHERE app_code = ? AND user_id = ?`, @@ -275,6 +276,40 @@ func (r *Repository) activateUserVip(ctx context.Context, tx *sql.Tx, command vi ); err != nil { return vipActivationResult{}, err } + var rewardItems []ledger.VipRewardItem + var resourceGrantID string + if command.Program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 { + // tiered 资源直接来自 active benefit bindings;reward group 不再是 entitlement 的人工同步前置条件。 + rewardItems, resourceGrantID, err = r.grantPaidVipBenefitResourcesTx(ctx, tx, activationVIP, command.NowMS) + } else { + rewardItems, resourceGrantID, err = r.applyVipRewardGroup(ctx, tx, vipRewardGrantCommand{ + AppCode: command.AppCode, CommandID: command.CommandID, UserID: command.UserID, + Source: command.Source, Reason: command.Reason, OperatorUserID: command.OperatorUserID, + }, command.Level, expiresAt, command.NowMS) + } + if err != nil { + return vipActivationResult{}, err + } + if command.Program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 { + state, stateErr := r.getVipStateFromSnapshot(ctx, tx, command.UserID, command.NowMS, command.Program) + if stateErr != nil { + return vipActivationResult{}, stateErr + } + if state.EffectiveSource == ledger.VipEffectiveSourceTrial && state.EquippedTrialCard != nil { + if err := r.grantVipTrialBenefitResourcesTx(ctx, tx, *state.EquippedTrialCard, command.Program.ConfigVersion, command.NowMS); err != nil { + return vipActivationResult{}, err + } + } + effectiveLevel := int32(0) + effectiveSource := ledger.VipEffectiveSourceNone + if state.EffectiveVip.Active { + effectiveLevel = state.EffectiveVip.Level + effectiveSource = state.EffectiveSource + } + if err := r.reconcileTieredVipResourceEquipmentTx(ctx, tx, command.UserID, effectiveLevel, effectiveSource, command.CommandID, command.NowMS); err != nil { + return vipActivationResult{}, err + } + } action := vipActivationAction(command.Source, previousLevel, command.Level.Level) if _, err := tx.ExecContext(ctx, ` INSERT INTO user_vip_history ( @@ -288,17 +323,7 @@ func (r *Repository) activateUserVip(ctx context.Context, tx *sql.Tx, command vi return vipActivationResult{}, err } return vipActivationResult{ - VIP: ledger.UserVip{ - UserID: command.UserID, - Level: command.Level.Level, - Name: command.Level.Name, - Active: true, - StartedAtMS: startedAt, - ExpiresAtMS: expiresAt, - UpdatedAtMS: command.NowMS, - ProgramType: command.Program.ProgramType, - ConfigVersion: command.Program.ConfigVersion, - }, + VIP: activationVIP, PreviousLevel: previousLevel, PreviousExpiresAtMS: command.Current.ExpiresAtMS, ExpiryPolicy: command.ExpiryPolicy, @@ -380,7 +405,7 @@ func (r *Repository) applyVipRewardGroup(ctx context.Context, tx *sql.Tx, comman if err != nil { return nil, "", err } - rewardCommandID := "vip_reward:" + command.CommandID + rewardCommandID := boundedDerivedCommandID("vip_reward:", command.CommandID) grantID := resourceGrantID(command.AppCode, rewardCommandID) source := ledger.NormalizeVipGrantSource(command.Source) if source == "" { @@ -422,7 +447,9 @@ func (r *Repository) applyVipRewardGroup(ctx context.Context, tx *sql.Tx, comman if resourcedomain.NormalizeResourceType(item.Resource.ResourceType) == resourcedomain.TypeVIPTrialCard { return nil, "", xerr.New(xerr.InvalidArgument, "vip reward group cannot contain vip_trial_card") } - inserted, err := r.applyGrantItem(ctx, tx, grantID, rewardCommandID, command.UserID, item.Resource, item.Quantity, durationMS, nowMs) + // VIP 每次生效来源(购买/直赠/体验卡)必须拥有独立 entitlement。资源目录常用 + // increase_quantity/extend_expiry,但跨来源合并会让体验卡缩短付费权益或让低等级继续引用高等级素材。 + inserted, err := r.applyGrantItemWithEntitlementStrategy(ctx, tx, grantID, rewardCommandID, command.UserID, item.Resource, item.Quantity, durationMS, nowMs, resourcedomain.GrantStrategyNewEntitlement) if err != nil { return nil, "", err } diff --git a/services/wallet-service/internal/storage/mysql/vip_query_repository.go b/services/wallet-service/internal/storage/mysql/vip_query_repository.go index 3d9d763d..c9dc010c 100644 --- a/services/wallet-service/internal/storage/mysql/vip_query_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_query_repository.go @@ -34,6 +34,9 @@ func (r *Repository) ListVipPackages(ctx context.Context, userID int64) (ledger. if err := r.pruneExpiredUserResourceEquipment(ctx, []int64{userID}, []string{resourcedomain.TypeVIPTrialCard}, nowMS); err != nil { return ledger.VipState{}, nil, err } + if err := r.ensureTieredVipResourceState(ctx, userID, nowMS); err != nil { + return ledger.VipState{}, nil, err + } tx, err := r.db.BeginTx(ctx, &sql.TxOptions{ReadOnly: true, Isolation: sql.LevelRepeatableRead}) if err != nil { return ledger.VipState{}, nil, err diff --git a/services/wallet-service/internal/storage/mysql/vip_trial_card_repository.go b/services/wallet-service/internal/storage/mysql/vip_trial_card_repository.go index 48b2b716..fd9c053a 100644 --- a/services/wallet-service/internal/storage/mysql/vip_trial_card_repository.go +++ b/services/wallet-service/internal/storage/mysql/vip_trial_card_repository.go @@ -237,7 +237,23 @@ func (r *Repository) EquipVipTrialCard(ctx context.Context, command ledger.Equip } defer func() { _ = tx.Rollback() }() nowMS := time.Now().UnixMilli() - program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL(), command.AppCode)) + var equipmentCommand resourcedomain.EquipUserResourceCommand + if command.EquipmentCommandID != "" { + equipmentCommand = resourcedomain.EquipUserResourceCommand{ + AppCode: command.AppCode, CommandID: command.EquipmentCommandID, UserID: command.UserID, + ResourceID: command.ResourceID, EntitlementID: command.EntitlementID, + } + // 通用入口的命令 guard 必须先于 program、entitlement 和 VIP 有效态读取加锁; + // 并发重试会等待首事务并读取快照,不会在到期边界重新执行状态机。 + _, replayed, err := r.lockResourceEquipmentCommand(ctx, tx, equipmentCommand, nowMS) + if err != nil { + return ledger.VipTrialCard{}, ledger.VipState{}, err + } + if replayed { + return ledger.VipTrialCard{}, ledger.VipState{}, nil + } + } + program, err := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, command.AppCode)) if errors.Is(err, sql.ErrNoRows) { return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPProgramInactive, "vip program config not found") } @@ -304,6 +320,22 @@ func (r *Repository) EquipVipTrialCard(ctx context.Context, command ledger.Equip return ledger.VipTrialCard{}, ledger.VipState{}, err } } + if program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 { + // 体验卡只下发当前等级显式绑定的真实 benefit resources,不复用可能含钱包奖励的付费资源组; + // grant_id 由 trial_card_id 稳定派生,来源和有效期与付费会员 entitlement 完全隔离。 + if err := r.grantVipTrialBenefitResourcesTx(ctx, tx, card, program.ConfigVersion, nowMS); err != nil { + return ledger.VipTrialCard{}, ledger.VipState{}, err + } + if err := r.reconcileTieredVipResourceEquipmentTx(ctx, tx, command.UserID, card.Level, ledger.VipEffectiveSourceTrial, command.RequestID, nowMS); err != nil { + return ledger.VipTrialCard{}, ledger.VipState{}, err + } + } + if command.EquipmentCommandID != "" { + entitlement.Equipped = true + if err := r.completeResourceEquipmentCommandTx(ctx, tx, equipmentCommand, entitlement, nowMS); err != nil { + return ledger.VipTrialCard{}, ledger.VipState{}, err + } + } if err := tx.Commit(); err != nil { return ledger.VipTrialCard{}, ledger.VipState{}, err } @@ -338,6 +370,33 @@ func (r *Repository) UnequipVipTrialCard(ctx context.Context, command ledger.Une return false, ledger.VipState{}, err } if affected > 0 { + program, programErr := r.queryVipProgramConfig(ctx, tx.QueryRowContext(ctx, vipProgramSelectSQL()+` LOCK IN SHARE MODE`, command.AppCode)) + if programErr != nil && !errors.Is(programErr, sql.ErrNoRows) { + return false, ledger.VipState{}, programErr + } + if programErr == nil && program.ProgramType == ledger.VipProgramTypeTieredPrivilegeV1 { + effectiveLevel := int32(0) + paid, paidErr := r.queryUserVip(ctx, tx, command.UserID, nowMS, false) + if paidErr != nil && !errors.Is(paidErr, sql.ErrNoRows) { + return false, ledger.VipState{}, paidErr + } + if paidErr == nil && program.Status == ledger.VipStatusActive && paid.Active { + effectiveLevel = paid.Level + paid.ProgramType = program.ProgramType + paid.ConfigVersion = program.ConfigVersion + if _, _, err := r.grantPaidVipBenefitResourcesTx(ctx, tx, paid, nowMS); err != nil { + return false, ledger.VipState{}, err + } + } + // 卸下体验卡后回落到付费会员或 none;同事务清掉体验卡等级资源并恢复付费等级 auto_equip。 + effectiveSource := ledger.VipEffectiveSourceNone + if effectiveLevel > 0 { + effectiveSource = ledger.VipEffectiveSourcePaid + } + if err := r.reconcileTieredVipResourceEquipmentTx(ctx, tx, command.UserID, effectiveLevel, effectiveSource, command.RequestID, nowMS); err != nil { + return false, ledger.VipState{}, err + } + } if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{ resourceOutboxEvent("UserResourceChanged", command.RequestID, command.UserID, 0, map[string]any{ "action": "unequip", diff --git a/services/wallet-service/internal/transport/grpc/resource.go b/services/wallet-service/internal/transport/grpc/resource.go index 69ebdf72..a6ba58f0 100644 --- a/services/wallet-service/internal/transport/grpc/resource.go +++ b/services/wallet-service/internal/transport/grpc/resource.go @@ -384,6 +384,7 @@ func (s *Server) ListUserResources(ctx context.Context, req *walletv1.ListUserRe func (s *Server) EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error) { item, err := s.svc.EquipUserResource(ctx, resourcedomain.EquipUserResourceCommand{ RequestID: req.GetRequestId(), + CommandID: req.GetCommandId(), AppCode: req.GetAppCode(), UserID: req.GetUserId(), ResourceID: req.GetResourceId(), diff --git a/services/wallet-service/internal/transport/grpc/vip.go b/services/wallet-service/internal/transport/grpc/vip.go index 8e0305d5..88c4c610 100644 --- a/services/wallet-service/internal/transport/grpc/vip.go +++ b/services/wallet-service/internal/transport/grpc/vip.go @@ -62,11 +62,13 @@ func (s *Server) CheckVipBenefit(ctx context.Context, req *walletv1.CheckVipBene return nil, xerr.ToGRPCError(err) } return &walletv1.CheckVipBenefitResponse{ - Allowed: result.Allowed, - Benefit: vipBenefitToProto(result.Benefit), - State: vipStateToProto(result.State), - DenialReason: result.DenialReason, - EvaluatedAtMs: result.EvaluatedAtMS, + Allowed: result.Allowed, + Benefit: vipBenefitToProto(result.Benefit), + State: vipStateToProto(result.State), + DenialReason: result.DenialReason, + EvaluatedAtMs: result.EvaluatedAtMS, + RequiredLevel: result.RequiredLevel, + CurrentEffectiveLevel: result.CurrentEffectiveLevel, }, nil } @@ -461,6 +463,27 @@ func vipBenefitToProto(benefit ledger.VipBenefit) *walletv1.VipBenefit { CreatedAtMs: benefit.CreatedAtMS, UpdatedAtMs: benefit.UpdatedAtMS, Presentation: vipBenefitPresentationToProto(benefit.Presentation), + Resource: vipBenefitResourceToProto(benefit.Resource), + } +} + +// vipBenefitResourceToProto 只转换 repository 已完成 active/type 校验的资源投影。 +// nil 必须继续保持 nil,使 Flutter 能区分“功能权益”与“装扮尚未完成真实资源配置”。 +func vipBenefitResourceToProto(resource *ledger.VipBenefitResource) *walletv1.Resource { + if resource == nil || resource.ResourceID <= 0 { + return nil + } + return &walletv1.Resource{ + AppCode: resource.AppCode, ResourceId: resource.ResourceID, ResourceCode: resource.ResourceCode, + ResourceType: resource.ResourceType, Name: resource.Name, Status: resource.Status, + Grantable: resource.Grantable, ManagerGrantEnabled: resource.ManagerGrantEnabled, + GrantStrategy: resource.GrantStrategy, WalletAssetType: resource.WalletAssetType, + WalletAssetAmount: resource.WalletAssetAmount, PriceType: resource.PriceType, + CoinPrice: resource.CoinPrice, GiftPointAmount: resource.GiftPointAmount, + UsageScopes: resource.UsageScopes, AssetUrl: resource.AssetURL, PreviewUrl: resource.PreviewURL, + AnimationUrl: resource.AnimationURL, MetadataJson: resource.MetadataJSON, SortOrder: resource.SortOrder, + CreatedByUserId: resource.CreatedByUserID, UpdatedByUserId: resource.UpdatedByUserID, + CreatedAtMs: resource.CreatedAtMS, UpdatedAtMs: resource.UpdatedAtMS, } }