房间背景图

This commit is contained in:
zhx 2026-05-28 17:57:29 +08:00
parent 31f686bd68
commit 283065e41d
26 changed files with 2727 additions and 660 deletions

View File

@ -203,14 +203,15 @@ func (x *RoomCreated) GetRoomDescription() string {
// RoomProfileUpdated 表达房间展示资料或麦位数量变更。
type RoomProfileUpdated 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"`
RoomName string `protobuf:"bytes,2,opt,name=room_name,json=roomName,proto3" json:"room_name,omitempty"`
RoomAvatar string `protobuf:"bytes,3,opt,name=room_avatar,json=roomAvatar,proto3" json:"room_avatar,omitempty"`
RoomDescription string `protobuf:"bytes,4,opt,name=room_description,json=roomDescription,proto3" json:"room_description,omitempty"`
SeatCount int32 `protobuf:"varint,5,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
RoomName string `protobuf:"bytes,2,opt,name=room_name,json=roomName,proto3" json:"room_name,omitempty"`
RoomAvatar string `protobuf:"bytes,3,opt,name=room_avatar,json=roomAvatar,proto3" json:"room_avatar,omitempty"`
RoomDescription string `protobuf:"bytes,4,opt,name=room_description,json=roomDescription,proto3" json:"room_description,omitempty"`
SeatCount int32 `protobuf:"varint,5,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"`
RoomBackgroundUrl string `protobuf:"bytes,6,opt,name=room_background_url,json=roomBackgroundUrl,proto3" json:"room_background_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomProfileUpdated) Reset() {
@ -278,6 +279,74 @@ func (x *RoomProfileUpdated) GetSeatCount() int32 {
return 0
}
func (x *RoomProfileUpdated) GetRoomBackgroundUrl() string {
if x != nil {
return x.RoomBackgroundUrl
}
return ""
}
// RoomBackgroundChanged 表达房主切换当前房间背景图。
type RoomBackgroundChanged 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"`
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"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomBackgroundChanged) Reset() {
*x = RoomBackgroundChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RoomBackgroundChanged) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RoomBackgroundChanged) ProtoMessage() {}
func (x *RoomBackgroundChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
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 RoomBackgroundChanged.ProtoReflect.Descriptor instead.
func (*RoomBackgroundChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{3}
}
func (x *RoomBackgroundChanged) GetActorUserId() int64 {
if x != nil {
return x.ActorUserId
}
return 0
}
func (x *RoomBackgroundChanged) GetBackgroundId() int64 {
if x != nil {
return x.BackgroundId
}
return 0
}
func (x *RoomBackgroundChanged) GetRoomBackgroundUrl() string {
if x != nil {
return x.RoomBackgroundUrl
}
return ""
}
// RoomUserJoined 表达用户业务态进房成功。
type RoomUserJoined struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -291,7 +360,7 @@ type RoomUserJoined struct {
func (x *RoomUserJoined) Reset() {
*x = RoomUserJoined{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -303,7 +372,7 @@ func (x *RoomUserJoined) String() string {
func (*RoomUserJoined) ProtoMessage() {}
func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -316,7 +385,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{3}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{4}
}
func (x *RoomUserJoined) GetUserId() int64 {
@ -350,7 +419,7 @@ type RoomUserLeft struct {
func (x *RoomUserLeft) Reset() {
*x = RoomUserLeft{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -362,7 +431,7 @@ func (x *RoomUserLeft) String() string {
func (*RoomUserLeft) ProtoMessage() {}
func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -375,7 +444,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{4}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{5}
}
func (x *RoomUserLeft) GetUserId() int64 {
@ -396,7 +465,7 @@ type RoomClosed struct {
func (x *RoomClosed) Reset() {
*x = RoomClosed{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -408,7 +477,7 @@ func (x *RoomClosed) String() string {
func (*RoomClosed) ProtoMessage() {}
func (x *RoomClosed) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -421,7 +490,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{5}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{6}
}
func (x *RoomClosed) GetActorUserId() int64 {
@ -462,7 +531,7 @@ type RoomMicChanged struct {
func (x *RoomMicChanged) Reset() {
*x = RoomMicChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -474,7 +543,7 @@ func (x *RoomMicChanged) String() string {
func (*RoomMicChanged) ProtoMessage() {}
func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -487,7 +556,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{6}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{7}
}
func (x *RoomMicChanged) GetActorUserId() int64 {
@ -586,7 +655,7 @@ type RoomMicSeatLocked struct {
func (x *RoomMicSeatLocked) Reset() {
*x = RoomMicSeatLocked{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -598,7 +667,7 @@ func (x *RoomMicSeatLocked) String() string {
func (*RoomMicSeatLocked) ProtoMessage() {}
func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -611,7 +680,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{7}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{8}
}
func (x *RoomMicSeatLocked) GetActorUserId() int64 {
@ -646,7 +715,7 @@ type RoomChatEnabledChanged struct {
func (x *RoomChatEnabledChanged) Reset() {
*x = RoomChatEnabledChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -658,7 +727,7 @@ func (x *RoomChatEnabledChanged) String() string {
func (*RoomChatEnabledChanged) ProtoMessage() {}
func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -671,7 +740,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{8}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{9}
}
func (x *RoomChatEnabledChanged) GetActorUserId() int64 {
@ -699,7 +768,7 @@ type RoomPasswordChanged struct {
func (x *RoomPasswordChanged) Reset() {
*x = RoomPasswordChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -711,7 +780,7 @@ func (x *RoomPasswordChanged) String() string {
func (*RoomPasswordChanged) ProtoMessage() {}
func (x *RoomPasswordChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -724,7 +793,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{9}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10}
}
func (x *RoomPasswordChanged) GetActorUserId() int64 {
@ -753,7 +822,7 @@ type RoomAdminChanged struct {
func (x *RoomAdminChanged) Reset() {
*x = RoomAdminChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -765,7 +834,7 @@ func (x *RoomAdminChanged) String() string {
func (*RoomAdminChanged) ProtoMessage() {}
func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -778,7 +847,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{10}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11}
}
func (x *RoomAdminChanged) GetActorUserId() int64 {
@ -814,7 +883,7 @@ type RoomUserMuted struct {
func (x *RoomUserMuted) Reset() {
*x = RoomUserMuted{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -826,7 +895,7 @@ func (x *RoomUserMuted) String() string {
func (*RoomUserMuted) ProtoMessage() {}
func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -839,7 +908,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{11}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12}
}
func (x *RoomUserMuted) GetActorUserId() int64 {
@ -874,7 +943,7 @@ type RoomUserKicked struct {
func (x *RoomUserKicked) Reset() {
*x = RoomUserKicked{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -886,7 +955,7 @@ func (x *RoomUserKicked) String() string {
func (*RoomUserKicked) ProtoMessage() {}
func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -899,7 +968,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{12}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13}
}
func (x *RoomUserKicked) GetActorUserId() int64 {
@ -927,7 +996,7 @@ type RoomUserUnbanned struct {
func (x *RoomUserUnbanned) Reset() {
*x = RoomUserUnbanned{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -939,7 +1008,7 @@ func (x *RoomUserUnbanned) String() string {
func (*RoomUserUnbanned) ProtoMessage() {}
func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -952,7 +1021,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{13}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14}
}
func (x *RoomUserUnbanned) GetActorUserId() int64 {
@ -987,7 +1056,7 @@ type RoomGiftSent struct {
func (x *RoomGiftSent) Reset() {
*x = RoomGiftSent{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -999,7 +1068,7 @@ func (x *RoomGiftSent) String() string {
func (*RoomGiftSent) ProtoMessage() {}
func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1012,7 +1081,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{14}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15}
}
func (x *RoomGiftSent) GetSenderUserId() int64 {
@ -1089,7 +1158,7 @@ type RoomHeatChanged struct {
func (x *RoomHeatChanged) Reset() {
*x = RoomHeatChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1101,7 +1170,7 @@ func (x *RoomHeatChanged) String() string {
func (*RoomHeatChanged) ProtoMessage() {}
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1114,7 +1183,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{15}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{16}
}
func (x *RoomHeatChanged) GetDelta() int64 {
@ -1143,7 +1212,7 @@ type RoomRankChanged struct {
func (x *RoomRankChanged) Reset() {
*x = RoomRankChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1155,7 +1224,7 @@ func (x *RoomRankChanged) String() string {
func (*RoomRankChanged) ProtoMessage() {}
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1168,7 +1237,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{16}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17}
}
func (x *RoomRankChanged) GetUserId() int64 {
@ -1208,7 +1277,7 @@ type RoomTreasureRewardGrant struct {
func (x *RoomTreasureRewardGrant) Reset() {
*x = RoomTreasureRewardGrant{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1220,7 +1289,7 @@ func (x *RoomTreasureRewardGrant) String() string {
func (*RoomTreasureRewardGrant) ProtoMessage() {}
func (x *RoomTreasureRewardGrant) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1233,7 +1302,7 @@ func (x *RoomTreasureRewardGrant) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureRewardGrant.ProtoReflect.Descriptor instead.
func (*RoomTreasureRewardGrant) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18}
}
func (x *RoomTreasureRewardGrant) GetRewardRole() string {
@ -1315,7 +1384,7 @@ type RoomTreasureProgressChanged struct {
func (x *RoomTreasureProgressChanged) Reset() {
*x = RoomTreasureProgressChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1327,7 +1396,7 @@ func (x *RoomTreasureProgressChanged) String() string {
func (*RoomTreasureProgressChanged) ProtoMessage() {}
func (x *RoomTreasureProgressChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1340,7 +1409,7 @@ func (x *RoomTreasureProgressChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureProgressChanged.ProtoReflect.Descriptor instead.
func (*RoomTreasureProgressChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
}
func (x *RoomTreasureProgressChanged) GetBoxId() string {
@ -1461,7 +1530,7 @@ type RoomTreasureCountdownStarted struct {
func (x *RoomTreasureCountdownStarted) Reset() {
*x = RoomTreasureCountdownStarted{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1473,7 +1542,7 @@ func (x *RoomTreasureCountdownStarted) String() string {
func (*RoomTreasureCountdownStarted) ProtoMessage() {}
func (x *RoomTreasureCountdownStarted) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1486,7 +1555,7 @@ func (x *RoomTreasureCountdownStarted) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureCountdownStarted.ProtoReflect.Descriptor instead.
func (*RoomTreasureCountdownStarted) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
}
func (x *RoomTreasureCountdownStarted) GetBoxId() string {
@ -1584,7 +1653,7 @@ type RoomTreasureOpened struct {
func (x *RoomTreasureOpened) Reset() {
*x = RoomTreasureOpened{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1596,7 +1665,7 @@ func (x *RoomTreasureOpened) String() string {
func (*RoomTreasureOpened) ProtoMessage() {}
func (x *RoomTreasureOpened) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1609,7 +1678,7 @@ func (x *RoomTreasureOpened) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureOpened.ProtoReflect.Descriptor instead.
func (*RoomTreasureOpened) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
}
func (x *RoomTreasureOpened) GetBoxId() string {
@ -1687,7 +1756,7 @@ type RoomTreasureRewardGranted struct {
func (x *RoomTreasureRewardGranted) Reset() {
*x = RoomTreasureRewardGranted{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1699,7 +1768,7 @@ func (x *RoomTreasureRewardGranted) String() string {
func (*RoomTreasureRewardGranted) ProtoMessage() {}
func (x *RoomTreasureRewardGranted) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1712,7 +1781,7 @@ func (x *RoomTreasureRewardGranted) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureRewardGranted.ProtoReflect.Descriptor instead.
func (*RoomTreasureRewardGranted) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22}
}
func (x *RoomTreasureRewardGranted) GetBoxId() string {
@ -1758,7 +1827,7 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
"\troom_name\x18\x05 \x01(\tR\broomName\x12\x1f\n" +
"\vroom_avatar\x18\x06 \x01(\tR\n" +
"roomAvatar\x12)\n" +
"\x10room_description\x18\a \x01(\tR\x0froomDescription\"\xc0\x01\n" +
"\x10room_description\x18\a \x01(\tR\x0froomDescription\"\xf0\x01\n" +
"\x12RoomProfileUpdated\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12\x1b\n" +
"\troom_name\x18\x02 \x01(\tR\broomName\x12\x1f\n" +
@ -1766,7 +1835,12 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
"roomAvatar\x12)\n" +
"\x10room_description\x18\x04 \x01(\tR\x0froomDescription\x12\x1d\n" +
"\n" +
"seat_count\x18\x05 \x01(\x05R\tseatCount\"i\n" +
"seat_count\x18\x05 \x01(\x05R\tseatCount\x12.\n" +
"\x13room_background_url\x18\x06 \x01(\tR\x11roomBackgroundUrl\"\x90\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\"i\n" +
"\x0eRoomUserJoined\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x12\n" +
"\x04role\x18\x02 \x01(\tR\x04role\x12*\n" +
@ -1911,34 +1985,35 @@ 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, 22)
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
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
(*RoomUserJoined)(nil), // 3: hyapp.events.room.v1.RoomUserJoined
(*RoomUserLeft)(nil), // 4: hyapp.events.room.v1.RoomUserLeft
(*RoomClosed)(nil), // 5: hyapp.events.room.v1.RoomClosed
(*RoomMicChanged)(nil), // 6: hyapp.events.room.v1.RoomMicChanged
(*RoomMicSeatLocked)(nil), // 7: hyapp.events.room.v1.RoomMicSeatLocked
(*RoomChatEnabledChanged)(nil), // 8: hyapp.events.room.v1.RoomChatEnabledChanged
(*RoomPasswordChanged)(nil), // 9: hyapp.events.room.v1.RoomPasswordChanged
(*RoomAdminChanged)(nil), // 10: hyapp.events.room.v1.RoomAdminChanged
(*RoomUserMuted)(nil), // 11: hyapp.events.room.v1.RoomUserMuted
(*RoomUserKicked)(nil), // 12: hyapp.events.room.v1.RoomUserKicked
(*RoomUserUnbanned)(nil), // 13: hyapp.events.room.v1.RoomUserUnbanned
(*RoomGiftSent)(nil), // 14: hyapp.events.room.v1.RoomGiftSent
(*RoomHeatChanged)(nil), // 15: hyapp.events.room.v1.RoomHeatChanged
(*RoomRankChanged)(nil), // 16: hyapp.events.room.v1.RoomRankChanged
(*RoomTreasureRewardGrant)(nil), // 17: hyapp.events.room.v1.RoomTreasureRewardGrant
(*RoomTreasureProgressChanged)(nil), // 18: hyapp.events.room.v1.RoomTreasureProgressChanged
(*RoomTreasureCountdownStarted)(nil), // 19: hyapp.events.room.v1.RoomTreasureCountdownStarted
(*RoomTreasureOpened)(nil), // 20: hyapp.events.room.v1.RoomTreasureOpened
(*RoomTreasureRewardGranted)(nil), // 21: hyapp.events.room.v1.RoomTreasureRewardGranted
(*RoomBackgroundChanged)(nil), // 3: hyapp.events.room.v1.RoomBackgroundChanged
(*RoomUserJoined)(nil), // 4: hyapp.events.room.v1.RoomUserJoined
(*RoomUserLeft)(nil), // 5: hyapp.events.room.v1.RoomUserLeft
(*RoomClosed)(nil), // 6: hyapp.events.room.v1.RoomClosed
(*RoomMicChanged)(nil), // 7: hyapp.events.room.v1.RoomMicChanged
(*RoomMicSeatLocked)(nil), // 8: hyapp.events.room.v1.RoomMicSeatLocked
(*RoomChatEnabledChanged)(nil), // 9: hyapp.events.room.v1.RoomChatEnabledChanged
(*RoomPasswordChanged)(nil), // 10: hyapp.events.room.v1.RoomPasswordChanged
(*RoomAdminChanged)(nil), // 11: hyapp.events.room.v1.RoomAdminChanged
(*RoomUserMuted)(nil), // 12: hyapp.events.room.v1.RoomUserMuted
(*RoomUserKicked)(nil), // 13: hyapp.events.room.v1.RoomUserKicked
(*RoomUserUnbanned)(nil), // 14: hyapp.events.room.v1.RoomUserUnbanned
(*RoomGiftSent)(nil), // 15: hyapp.events.room.v1.RoomGiftSent
(*RoomHeatChanged)(nil), // 16: hyapp.events.room.v1.RoomHeatChanged
(*RoomRankChanged)(nil), // 17: hyapp.events.room.v1.RoomRankChanged
(*RoomTreasureRewardGrant)(nil), // 18: hyapp.events.room.v1.RoomTreasureRewardGrant
(*RoomTreasureProgressChanged)(nil), // 19: hyapp.events.room.v1.RoomTreasureProgressChanged
(*RoomTreasureCountdownStarted)(nil), // 20: hyapp.events.room.v1.RoomTreasureCountdownStarted
(*RoomTreasureOpened)(nil), // 21: hyapp.events.room.v1.RoomTreasureOpened
(*RoomTreasureRewardGranted)(nil), // 22: hyapp.events.room.v1.RoomTreasureRewardGranted
}
var file_proto_events_room_v1_events_proto_depIdxs = []int32{
17, // 0: hyapp.events.room.v1.RoomTreasureOpened.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
17, // 1: hyapp.events.room.v1.RoomTreasureRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
18, // 0: hyapp.events.room.v1.RoomTreasureOpened.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
18, // 1: hyapp.events.room.v1.RoomTreasureRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
@ -1957,7 +2032,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: 22,
NumMessages: 23,
NumExtensions: 0,
NumServices: 0,
},

View File

@ -34,6 +34,14 @@ message RoomProfileUpdated {
string room_avatar = 3;
string room_description = 4;
int32 seat_count = 5;
string room_background_url = 6;
}
// RoomBackgroundChanged
message RoomBackgroundChanged {
int64 actor_user_id = 1;
int64 background_id = 2;
string room_background_url = 3;
}
// RoomUserJoined

File diff suppressed because it is too large Load Diff

View File

@ -324,6 +324,51 @@ message RoomSnapshot {
RoomTreasureState treasure = 20;
}
// RoomBackgroundImage
// room-service RoomSnapshot.room_ext["background_url"]
message RoomBackgroundImage {
int64 background_id = 1;
string room_id = 2;
string image_url = 3;
int64 created_by_user_id = 4;
int64 created_at_ms = 5;
int64 updated_at_ms = 6;
}
message SaveRoomBackgroundRequest {
RequestMeta meta = 1;
string room_id = 2;
string image_url = 3;
}
message SaveRoomBackgroundResponse {
RoomBackgroundImage background = 1;
int64 server_time_ms = 2;
}
message ListRoomBackgroundsRequest {
RequestMeta meta = 1;
string room_id = 2;
int64 viewer_user_id = 3;
}
message ListRoomBackgroundsResponse {
repeated RoomBackgroundImage backgrounds = 1;
string selected_background_url = 2;
int64 server_time_ms = 3;
}
message SetRoomBackgroundRequest {
RequestMeta meta = 1;
int64 background_id = 2;
}
message SetRoomBackgroundResponse {
CommandResult result = 1;
RoomSnapshot room = 2;
RoomBackgroundImage background = 3;
}
// CreateRoomRequest
// meta.room_idmeta.command_idmeta.actor_user_idmode room_name
// meta.actor_user_id owner Conflict
@ -959,6 +1004,8 @@ message UnfollowRoomResponse {
service RoomCommandService {
rpc CreateRoom(CreateRoomRequest) returns (CreateRoomResponse);
rpc UpdateRoomProfile(UpdateRoomProfileRequest) returns (UpdateRoomProfileResponse);
rpc SaveRoomBackground(SaveRoomBackgroundRequest) returns (SaveRoomBackgroundResponse);
rpc SetRoomBackground(SetRoomBackgroundRequest) returns (SetRoomBackgroundResponse);
rpc JoinRoom(JoinRoomRequest) returns (JoinRoomResponse);
rpc RoomHeartbeat(RoomHeartbeatRequest) returns (RoomHeartbeatResponse);
rpc LeaveRoom(LeaveRoomRequest) returns (LeaveRoomResponse);
@ -1007,6 +1054,7 @@ service RoomQueryService {
rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse);
rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse);
rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse);
rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse);
rpc GetRoomTreasure(GetRoomTreasureRequest) returns (GetRoomTreasureResponse);
rpc ListRoomOnlineUsers(ListRoomOnlineUsersRequest) returns (ListRoomOnlineUsersResponse);
}

View File

@ -21,6 +21,8 @@ const _ = grpc.SupportPackageIsVersion9
const (
RoomCommandService_CreateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CreateRoom"
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_JoinRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/JoinRoom"
RoomCommandService_RoomHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/RoomHeartbeat"
RoomCommandService_LeaveRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/LeaveRoom"
@ -58,6 +60,8 @@ const (
type RoomCommandServiceClient interface {
CreateRoom(ctx context.Context, in *CreateRoomRequest, opts ...grpc.CallOption) (*CreateRoomResponse, error)
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)
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)
@ -115,6 +119,26 @@ func (c *roomCommandServiceClient) UpdateRoomProfile(ctx context.Context, in *Up
return out, nil
}
func (c *roomCommandServiceClient) SaveRoomBackground(ctx context.Context, in *SaveRoomBackgroundRequest, opts ...grpc.CallOption) (*SaveRoomBackgroundResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SaveRoomBackgroundResponse)
err := c.cc.Invoke(ctx, RoomCommandService_SaveRoomBackground_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomCommandServiceClient) SetRoomBackground(ctx context.Context, in *SetRoomBackgroundRequest, opts ...grpc.CallOption) (*SetRoomBackgroundResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SetRoomBackgroundResponse)
err := c.cc.Invoke(ctx, RoomCommandService_SetRoomBackground_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)
@ -393,6 +417,8 @@ func (c *roomCommandServiceClient) UnfollowRoom(ctx context.Context, in *Unfollo
type RoomCommandServiceServer interface {
CreateRoom(context.Context, *CreateRoomRequest) (*CreateRoomResponse, error)
UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error)
SaveRoomBackground(context.Context, *SaveRoomBackgroundRequest) (*SaveRoomBackgroundResponse, error)
SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error)
JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error)
RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error)
LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, error)
@ -436,6 +462,12 @@ func (UnimplementedRoomCommandServiceServer) CreateRoom(context.Context, *Create
func (UnimplementedRoomCommandServiceServer) UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRoomProfile not implemented")
}
func (UnimplementedRoomCommandServiceServer) SaveRoomBackground(context.Context, *SaveRoomBackgroundRequest) (*SaveRoomBackgroundResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SaveRoomBackground not implemented")
}
func (UnimplementedRoomCommandServiceServer) SetRoomBackground(context.Context, *SetRoomBackgroundRequest) (*SetRoomBackgroundResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SetRoomBackground not implemented")
}
func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method JoinRoom not implemented")
}
@ -574,6 +606,42 @@ func _RoomCommandService_UpdateRoomProfile_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_SaveRoomBackground_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SaveRoomBackgroundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomCommandServiceServer).SaveRoomBackground(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomCommandService_SaveRoomBackground_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).SaveRoomBackground(ctx, req.(*SaveRoomBackgroundRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_SetRoomBackground_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SetRoomBackgroundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomCommandServiceServer).SetRoomBackground(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomCommandService_SetRoomBackground_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).SetRoomBackground(ctx, req.(*SetRoomBackgroundRequest))
}
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 {
@ -1075,6 +1143,14 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
MethodName: "UpdateRoomProfile",
Handler: _RoomCommandService_UpdateRoomProfile_Handler,
},
{
MethodName: "SaveRoomBackground",
Handler: _RoomCommandService_SaveRoomBackground_Handler,
},
{
MethodName: "SetRoomBackground",
Handler: _RoomCommandService_SetRoomBackground_Handler,
},
{
MethodName: "JoinRoom",
Handler: _RoomCommandService_JoinRoom_Handler,
@ -1344,6 +1420,7 @@ const (
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot"
RoomQueryService_ListRoomBackgrounds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBackgrounds"
RoomQueryService_GetRoomTreasure_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomTreasure"
RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers"
)
@ -1365,6 +1442,7 @@ type RoomQueryServiceClient interface {
GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error)
GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error)
GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, error)
ListRoomBackgrounds(ctx context.Context, in *ListRoomBackgroundsRequest, opts ...grpc.CallOption) (*ListRoomBackgroundsResponse, error)
GetRoomTreasure(ctx context.Context, in *GetRoomTreasureRequest, opts ...grpc.CallOption) (*GetRoomTreasureResponse, error)
ListRoomOnlineUsers(ctx context.Context, in *ListRoomOnlineUsersRequest, opts ...grpc.CallOption) (*ListRoomOnlineUsersResponse, error)
}
@ -1487,6 +1565,16 @@ func (c *roomQueryServiceClient) GetRoomSnapshot(ctx context.Context, in *GetRoo
return out, nil
}
func (c *roomQueryServiceClient) ListRoomBackgrounds(ctx context.Context, in *ListRoomBackgroundsRequest, opts ...grpc.CallOption) (*ListRoomBackgroundsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRoomBackgroundsResponse)
err := c.cc.Invoke(ctx, RoomQueryService_ListRoomBackgrounds_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomQueryServiceClient) GetRoomTreasure(ctx context.Context, in *GetRoomTreasureRequest, opts ...grpc.CallOption) (*GetRoomTreasureResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetRoomTreasureResponse)
@ -1524,6 +1612,7 @@ type RoomQueryServiceServer interface {
GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error)
GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error)
GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error)
ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error)
GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error)
ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error)
mustEmbedUnimplementedRoomQueryServiceServer()
@ -1569,6 +1658,9 @@ func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetC
func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRoomSnapshot not implemented")
}
func (UnimplementedRoomQueryServiceServer) ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomBackgrounds not implemented")
}
func (UnimplementedRoomQueryServiceServer) GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRoomTreasure not implemented")
}
@ -1794,6 +1886,24 @@ func _RoomQueryService_GetRoomSnapshot_Handler(srv interface{}, ctx context.Cont
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_ListRoomBackgrounds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRoomBackgroundsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).ListRoomBackgrounds(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_ListRoomBackgrounds_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).ListRoomBackgrounds(ctx, req.(*ListRoomBackgroundsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomQueryService_GetRoomTreasure_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetRoomTreasureRequest)
if err := dec(in); err != nil {
@ -1881,6 +1991,10 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetRoomSnapshot",
Handler: _RoomQueryService_GetRoomSnapshot_Handler,
},
{
MethodName: "ListRoomBackgrounds",
Handler: _RoomQueryService_ListRoomBackgrounds_Handler,
},
{
MethodName: "GetRoomTreasure",
Handler: _RoomQueryService_GetRoomTreasure_Handler,

View File

@ -0,0 +1,385 @@
# 房间背景图 Flutter 对接
本文描述 Flutter App 对接房间背景图的 HTTP 接口和腾讯云 IM 房间消息。房间背景图是房间维度素材:上传文件只拿 URL保存接口把 URL 归档到房间素材列表,设置接口才会改变当前房间背景并广播房间 IM 消息。
## 基础地址
本地开发地址:
```text
http://127.0.0.1:13000
```
线上环境使用当前 App 配置里的 gateway base URL。下面所有路径都拼在 gateway base URL 后。
## 请求头
| Header | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token服务端从 token 读取当前用户 ID。 |
| `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
| `Content-Type` | POST JSON 必填 | `application/json` | 保存和设置背景使用 JSON。 |
| `Content-Type` | 上传必填 | `multipart/form-data` | 上传文件使用 multipart。 |
统一响应 envelope
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {}
}
```
客户端分支判断只看 `code``message` 只用于兜底展示,`request_id` 要写入日志。
## 调用顺序
```text
1. 房主选择本地图片
2. POST /api/v1/files/upload 上传图片,拿 data.url
3. POST /api/v1/rooms/backgrounds/save 保存到房间背景素材列表
4. GET /api/v1/rooms/{room_id}/backgrounds 刷新背景列表
5. POST /api/v1/rooms/backgrounds/set 设置当前背景
6. 当前房间内所有用户通过 IM 收到 room_background_changed
7. 房间页用 data.room.background_url 或 IM attributes.room_background_url 刷新背景
```
## 上传图片
背景图先走通用文件上传。该接口不改变房间状态,只返回对象存储 URL。
```http
POST /api/v1/files/upload
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: multipart/form-data
file=<binary>
```
请求字段:
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `file` | 是 | multipart file | 单文件最大 20MB。背景建议传图片文件服务端通用入口不绑定房间语义。 |
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_upload_bg",
"data": {
"url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
"object_key": "app/files/10001/1778000000000_xxx.png",
"content_type": "image/png",
"size_bytes": 345678
}
}
```
客户端只需要把 `data.url` 传给保存背景接口。
## 保存背景图
保存背景图把 URL 写入当前房间的背景素材列表。保存成功不会改变当前房间背景,也不会发房间 IM 消息。
```http
POST /api/v1/rooms/backgrounds/save
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"room_id": "lalu_abc123",
"image_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png"
}
```
请求字段:
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `room_id` | 是 | string | 房间 ID。 |
| `image_url` | 是 | string | 上传接口返回的 `data.url`;服务端 trim 后最长 1024 字符。 |
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_bg_save",
"data": {
"background": {
"background_id": 101,
"room_id": "lalu_abc123",
"image_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
"created_by_user_id": "10001",
"created_at_ms": 1778000000123,
"updated_at_ms": 1778000000123
},
"server_time_ms": 1778000000123
}
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `background.background_id` | int64 | 设置当前背景时使用。Flutter 如果统一按字符串保存 ID也可以转成字符串保存提交时服务端支持数字或数字字符串。 |
| `background.image_url` | string | 背景图展示 URL。 |
| `background.created_by_user_id` | string | 保存素材的用户 ID。 |
| `server_time_ms` | int64 | 服务端时间Unix epoch milliseconds。 |
## 获取背景图列表
房主打开房间背景编辑面板时调用。
```http
GET /api/v1/rooms/{room_id}/backgrounds
Authorization: Bearer <access_token>
X-App-Code: lalu
```
路径参数:
| 参数 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `room_id` | 是 | string | 房间 ID。 |
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_bg_list",
"data": {
"backgrounds": [
{
"background_id": 101,
"room_id": "lalu_abc123",
"image_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
"created_by_user_id": "10001",
"created_at_ms": 1778000000123,
"updated_at_ms": 1778000000123
}
],
"selected_background_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
"server_time_ms": 1778000000456
}
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `backgrounds` | array | 房间保存过的背景图,按最近保存优先。 |
| `selected_background_url` | string | 当前生效背景;没有设置过时为空或缺省。 |
| `server_time_ms` | int64 | 服务端时间。 |
## 设置当前背景
设置当前背景会改变 Room Cell 快照,返回最新房间数据,并向房间 IM 群广播 `room_background_changed`
```http
POST /api/v1/rooms/backgrounds/set
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"room_id": "lalu_abc123",
"command_id": "cmd_background_lalu_abc123_10001_1778000000000",
"background_id": "101"
}
```
请求字段:
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `room_id` | 是 | string | 房间 ID。 |
| `command_id` | 是 | string | 房间命令幂等键;同一次点击超时重试必须复用。 |
| `background_id` | 是 | int64/string | 背景素材 ID必须来自当前房间的背景列表。 |
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_bg_set",
"data": {
"result": {
"applied": true,
"room_version": 18,
"server_time_ms": 1778000000789
},
"room": {
"room_id": "lalu_abc123",
"im_group_id": "lalu_abc123",
"room_short_id": "100001",
"title": "Live Room",
"cover_url": "https://cdn.example.com/room.png",
"background_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
"description": "welcome",
"owner_user_id": "10001",
"mode": "voice",
"status": "active",
"chat_enabled": true,
"locked": false,
"heat": 1000,
"online_count": 12,
"seat_count": 10,
"occupied_seat_count": 2,
"visible_region_id": 1001,
"version": 18
},
"background": {
"background_id": 101,
"room_id": "lalu_abc123",
"image_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
"created_by_user_id": "10001",
"created_at_ms": 1778000000123,
"updated_at_ms": 1778000000123
},
"server_time_ms": 1778000000789
}
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `result.applied` | bool | `true` 表示本次命令产生状态变更;同一 `command_id` 重试可能为 `false`。 |
| `result.room_version` | int64 | 命令完成后的房间版本。 |
| `room.background_url` | string | 当前生效背景图。客户端设置成功后直接用它刷新 UI。 |
| `background` | object | 被设置的背景素材。 |
## 房间详情返回
进房或详情页重建时不需要额外查背景列表。当前生效背景在房间详情里返回:
```http
GET /api/v1/rooms/{room_id}/detail
Authorization: Bearer <access_token>
X-App-Code: lalu
```
关键字段:
```json
{
"data": {
"room": {
"room_id": "lalu_abc123",
"background_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png",
"version": 18
}
}
}
```
客户端进入房间页时用 `data.room.background_url` 初始化背景。收到 IM 后如果 `room_version` 更大,再覆盖本地背景。
## 腾讯 IM 消息
设置背景成功后room-service 会向房间 IM 群发送 TIMCustomElem。Flutter 需要监听群自定义消息并解析 JSON。
消息负载是 `tencentim.RoomEvent` JSON
```json
{
"event_id": "evt_xxx",
"room_id": "lalu_abc123",
"event_type": "room_background_changed",
"actor_user_id": 10001,
"room_version": 18,
"attributes": {
"background_id": "101",
"room_background_url": "https://cdn.example.com/app/files/10001/1778000000000_xxx.png"
}
}
```
腾讯 IM 自定义消息字段:
| 字段 | 值 | 说明 |
| --- | --- | --- |
| `TIMCustomElem.data` | 上面的 JSON 字符串 | 业务负载。 |
| `TIMCustomElem.desc` | `room_background_changed` | 事件类型。 |
| `TIMCustomElem.extension` | `room_system_message` | 房间系统消息标记。 |
客户端处理规则:
| 条件 | 处理 |
| --- | --- |
| `event_type != room_background_changed` | 交给其他房间消息处理器。 |
| `room_id` 不是当前房间 | 忽略。 |
| `event_id` 已处理过 | 忽略,避免重放或 SDK 重复投递。 |
| `room_version` 小于本地房间版本 | 忽略旧消息。 |
| `room_version` 大于等于本地房间版本 | 设置房间背景为 `attributes.room_background_url`,并更新本地 `room_version`。 |
Flutter 伪代码:
```dart
void onRoomCustomMessage(String data) {
final event = jsonDecode(data) as Map<String, dynamic>;
if (event['event_type'] != 'room_background_changed') return;
if (event['room_id'] != currentRoomId) return;
final eventId = event['event_id'] as String? ?? '';
if (handledEventIds.contains(eventId)) return;
final version = (event['room_version'] as num?)?.toInt() ?? 0;
if (version < localRoomVersion) return;
final attrs = (event['attributes'] as Map?)?.cast<String, dynamic>() ?? {};
final backgroundUrl = attrs['room_background_url'] as String? ?? '';
handledEventIds.add(eventId);
localRoomVersion = version;
setRoomBackground(backgroundUrl);
}
```
## 错误处理
| code | HTTP | 常见场景 | Flutter 处理 |
| --- | --- | --- | --- |
| `INVALID_ARGUMENT` | 400 | `room_id` 格式不对、`image_url` 为空、`background_id` 为空、上传文件过大或格式不合法。 | 停止当前动作,提示重新选择图片或刷新页面后重试。 |
| `UNAUTHORIZED` | 401 | access token 缺失或过期。 | 回登录或刷新 token。 |
| `PROFILE_REQUIRED` | 403 | 用户未完成资料。 | 引导补全资料。 |
| `PERMISSION_DENIED` | 403 | 当前用户不是房主。 | 关闭编辑入口或提示无权限;不要重试。 |
| `NOT_FOUND` | 404 | 房间不存在,或设置的 `background_id` 不属于该房间。 | 刷新我的房间和背景列表。 |
| `CONFLICT` | 409 | 房间不是 active或同一 `command_id` 携带了不同 payload。 | 刷新房间状态;如果是超时重试,确保复用同一个请求体。 |
| `UPSTREAM_ERROR` | 502 | gateway、room-service、对象存储或 IM 投递依赖临时不可用。 | 保留用户选择,提示稍后重试。 |
失败响应示例:
```json
{
"code": "PERMISSION_DENIED",
"message": "permission denied",
"request_id": "req_bg_set_denied"
}
```
## UI 状态建议
| 场景 | 建议 |
| --- | --- |
| 房主打开背景设置面板 | 调 `GET /rooms/{room_id}/backgrounds`,用 `selected_background_url` 标记当前背景。 |
| 用户选择新图片 | 先本地预览,再上传,上传成功后调用保存接口。 |
| 保存成功 | 把返回的 `background` 插入列表顶部,但不切换房间背景。 |
| 点击应用背景 | 生成新的 `command_id` 调设置接口;按钮进入 loading防止重复点击。 |
| 设置成功 | 用响应里的 `room.background_url` 立即刷新当前设备 UI。 |
| 其他用户在线 | 通过 `room_background_changed` IM 消息刷新背景。 |
| 断线重进 | 通过 `/rooms/{room_id}/detail``room.background_url` 恢复当前背景,不依赖历史 IM。 |

View File

@ -60,10 +60,13 @@
| GET | `/api/v1/rooms/{room_id}/detail` | rooms | `getRoomDetail` | 查询房间详情页首屏聚合包 |
| GET | `/api/v1/rooms/{room_id}/online-users` | rooms | `listRoomOnlineUsers` | 分页查询房间在线用户 |
| GET | `/api/v1/rooms/{room_id}/gift-panel` | rooms | `getRoomGiftPanel` | 查询房间礼物面板初始化数据 |
| GET | `/api/v1/rooms/{room_id}/backgrounds` | rooms | `listRoomBackgrounds` | 房主查询房间已保存背景图列表和当前背景 |
| POST | `/api/v1/rooms/{room_id}/follow` | rooms | `followRoom` | 关注房间 |
| DELETE | `/api/v1/rooms/{room_id}/follow` | rooms | `unfollowRoom` | 取消关注房间 |
| POST | `/api/v1/rooms/create` | rooms | `createRoom` | 创建房间 |
| POST | `/api/v1/rooms/profile/update` | rooms | `updateRoomProfile` | 修改房间资料和座位数 |
| POST | `/api/v1/rooms/backgrounds/save` | rooms | `saveRoomBackground` | 房主保存房间背景图 URL 到素材列表 |
| POST | `/api/v1/rooms/backgrounds/set` | rooms | `setRoomBackground` | 房主设置当前房间背景图 |
| POST | `/api/v1/rooms/join` | rooms | `joinRoom` | 加入房间并返回首屏数据和 RTC token |
| POST | `/api/v1/rooms/heartbeat` | rooms | `roomHeartbeat` | 刷新房间业务 presence |
| POST | `/api/v1/rooms/leave` | rooms | `leaveRoom` | 离开房间 |

View File

@ -513,6 +513,54 @@ Rules:
- 成功响应返回最新 `room``seats`,客户端直接刷新房间标题、头像、简介和麦位列表。
- 成功后房间系统消息事件名为 `room_profile_updated`
### Room Backgrounds
房间背景图是房间维度素材;保存素材不立即改变当前房间背景,设置背景会写入 Room Cell 快照,`GET /api/v1/rooms/{room_id}/detail``data.room.background_url` 返回当前生效背景。
```http
POST /api/v1/rooms/backgrounds/save
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"room_id": "lalu_xxx",
"image_url": "https://cdn.example/room-bg.png"
}
```
```http
GET /api/v1/rooms/{room_id}/backgrounds
Authorization: Bearer <access_token>
```
Response `data.backgrounds[]` 返回 `background_id``image_url`、创建人和时间;`data.selected_background_url` 是当前生效背景。
```http
POST /api/v1/rooms/backgrounds/set
Authorization: Bearer <access_token>
Content-Type: application/json
```
Request:
```json
{
"room_id": "lalu_xxx",
"command_id": "cmd_background_<room_id>_<user_id>_<nonce>",
"background_id": "101"
}
```
Rules:
- 只有房主可以保存、查询和设置自己房间的背景图。
- `image_url` 应先通过上传接口拿到对象存储 URL再传给保存接口。
- 设置成功后返回最新 `room.background_url`,并通过 `room_background_changed` 房间系统消息同步;消息 `attributes` 包含 `background_id``room_background_url`
### Heartbeat
```http

View File

@ -11,6 +11,8 @@ import (
type RoomClient interface {
CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest) (*roomv1.CreateRoomResponse, error)
UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (*roomv1.UpdateRoomProfileResponse, error)
SaveRoomBackground(ctx context.Context, req *roomv1.SaveRoomBackgroundRequest) (*roomv1.SaveRoomBackgroundResponse, error)
SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBackgroundRequest) (*roomv1.SetRoomBackgroundResponse, error)
JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error)
RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRequest) (*roomv1.RoomHeartbeatResponse, error)
LeaveRoom(ctx context.Context, req *roomv1.LeaveRoomRequest) (*roomv1.LeaveRoomResponse, error)
@ -47,6 +49,7 @@ type RoomQueryClient interface {
GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error)
GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error)
GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, error)
ListRoomBackgrounds(ctx context.Context, req *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error)
GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error)
ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error)
}
@ -92,6 +95,14 @@ func (c *grpcRoomClient) UpdateRoomProfile(ctx context.Context, req *roomv1.Upda
return c.client.UpdateRoomProfile(ctx, req)
}
func (c *grpcRoomClient) SaveRoomBackground(ctx context.Context, req *roomv1.SaveRoomBackgroundRequest) (*roomv1.SaveRoomBackgroundResponse, error) {
return c.client.SaveRoomBackground(ctx, req)
}
func (c *grpcRoomClient) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBackgroundRequest) (*roomv1.SetRoomBackgroundResponse, error) {
return c.client.SetRoomBackground(ctx, req)
}
func (c *grpcRoomClient) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
return c.client.JoinRoom(ctx, req)
}
@ -200,6 +211,10 @@ func (c *grpcRoomQueryClient) GetRoomSnapshot(ctx context.Context, req *roomv1.G
return c.client.GetRoomSnapshot(ctx, req)
}
func (c *grpcRoomQueryClient) ListRoomBackgrounds(ctx context.Context, req *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) {
return c.client.ListRoomBackgrounds(ctx, req)
}
func (c *grpcRoomQueryClient) GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error) {
return c.client.GetRoomTreasure(ctx, req)
}

View File

@ -105,6 +105,9 @@ type RoomHandlers struct {
FollowRoom http.HandlerFunc
CreateRoom http.HandlerFunc
UpdateRoomProfile http.HandlerFunc
SaveRoomBackground http.HandlerFunc
ListRoomBackgrounds http.HandlerFunc
SetRoomBackground http.HandlerFunc
JoinRoom http.HandlerFunc
RoomHeartbeat http.HandlerFunc
LeaveRoom http.HandlerFunc
@ -312,8 +315,11 @@ func (r routes) registerRoomRoutes() {
r.profile("/rooms/{room_id}/gift-panel", http.MethodGet, h.GetRoomGiftPanel)
r.profile("/rooms/{room_id}/treasure", http.MethodGet, h.GetRoomTreasure)
r.profile("/rooms/{room_id}/follow", "", h.FollowRoom)
r.profile("/rooms/{room_id}/backgrounds", http.MethodGet, h.ListRoomBackgrounds)
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)
r.profile("/rooms/backgrounds/set", http.MethodPost, h.SetRoomBackground)
r.profile("/rooms/join", http.MethodPost, h.JoinRoom)
r.profile("/rooms/heartbeat", http.MethodPost, h.RoomHeartbeat)
r.profile("/rooms/leave", http.MethodPost, h.LeaveRoom)

View File

@ -33,32 +33,34 @@ import (
// fakeRoomClient 只承接 gateway transport 测试需要观察的 gRPC 入参。
// 测试目标是 HTTP envelope 和 RequestMeta 传递,不验证 room-service 业务行为。
type fakeRoomClient struct {
lastCreate *roomv1.CreateRoomRequest
lastUpdateProfile *roomv1.UpdateRoomProfileRequest
lastJoin *roomv1.JoinRoomRequest
lastHeartbeat *roomv1.RoomHeartbeatRequest
lastLeave *roomv1.LeaveRoomRequest
lastMicUp *roomv1.MicUpRequest
lastMicDown *roomv1.MicDownRequest
lastChangeMicSeat *roomv1.ChangeMicSeatRequest
lastConfirmMic *roomv1.ConfirmMicPublishingRequest
lastSetMicMute *roomv1.SetMicMuteRequest
lastRTCEvent *roomv1.ApplyRTCEventRequest
lastMicSeatLock *roomv1.SetMicSeatLockRequest
lastChatEnabled *roomv1.SetChatEnabledRequest
lastRoomPassword *roomv1.SetRoomPasswordRequest
lastSetAdmin *roomv1.SetRoomAdminRequest
lastMute *roomv1.MuteUserRequest
lastKick *roomv1.KickUserRequest
lastUnban *roomv1.UnbanUserRequest
lastGift *roomv1.SendGiftRequest
sendGiftResp *roomv1.SendGiftResponse
lastFollowRoom *roomv1.FollowRoomRequest
lastUnfollowRoom *roomv1.UnfollowRoomRequest
createErr error
updateProfileErr error
joinErr error
joinResp *roomv1.JoinRoomResponse
lastCreate *roomv1.CreateRoomRequest
lastUpdateProfile *roomv1.UpdateRoomProfileRequest
lastSaveBackground *roomv1.SaveRoomBackgroundRequest
lastSetBackground *roomv1.SetRoomBackgroundRequest
lastJoin *roomv1.JoinRoomRequest
lastHeartbeat *roomv1.RoomHeartbeatRequest
lastLeave *roomv1.LeaveRoomRequest
lastMicUp *roomv1.MicUpRequest
lastMicDown *roomv1.MicDownRequest
lastChangeMicSeat *roomv1.ChangeMicSeatRequest
lastConfirmMic *roomv1.ConfirmMicPublishingRequest
lastSetMicMute *roomv1.SetMicMuteRequest
lastRTCEvent *roomv1.ApplyRTCEventRequest
lastMicSeatLock *roomv1.SetMicSeatLockRequest
lastChatEnabled *roomv1.SetChatEnabledRequest
lastRoomPassword *roomv1.SetRoomPasswordRequest
lastSetAdmin *roomv1.SetRoomAdminRequest
lastMute *roomv1.MuteUserRequest
lastKick *roomv1.KickUserRequest
lastUnban *roomv1.UnbanUserRequest
lastGift *roomv1.SendGiftRequest
sendGiftResp *roomv1.SendGiftResponse
lastFollowRoom *roomv1.FollowRoomRequest
lastUnfollowRoom *roomv1.UnfollowRoomRequest
createErr error
updateProfileErr error
joinErr error
joinResp *roomv1.JoinRoomResponse
}
func (f *fakeRoomClient) CreateRoom(_ context.Context, req *roomv1.CreateRoomRequest) (*roomv1.CreateRoomResponse, error) {
@ -97,6 +99,47 @@ func (f *fakeRoomClient) UpdateRoomProfile(_ context.Context, req *roomv1.Update
}, nil
}
func (f *fakeRoomClient) SaveRoomBackground(_ context.Context, req *roomv1.SaveRoomBackgroundRequest) (*roomv1.SaveRoomBackgroundResponse, error) {
f.lastSaveBackground = req
return &roomv1.SaveRoomBackgroundResponse{
Background: &roomv1.RoomBackgroundImage{
BackgroundId: 101,
RoomId: req.GetRoomId(),
ImageUrl: req.GetImageUrl(),
CreatedByUserId: req.GetMeta().GetActorUserId(),
CreatedAtMs: 1_700_000_000_003,
UpdatedAtMs: 1_700_000_000_003,
},
ServerTimeMs: 1_700_000_000_003,
}, nil
}
func (f *fakeRoomClient) SetRoomBackground(_ context.Context, req *roomv1.SetRoomBackgroundRequest) (*roomv1.SetRoomBackgroundResponse, error) {
f.lastSetBackground = req
return &roomv1.SetRoomBackgroundResponse{
Result: &roomv1.CommandResult{Applied: true, RoomVersion: 4, ServerTimeMs: 1_700_000_000_004},
Room: &roomv1.RoomSnapshot{
RoomId: req.GetMeta().GetRoomId(),
OwnerUserId: req.GetMeta().GetActorUserId(),
Mode: "voice",
Status: "active",
ChatEnabled: true,
RoomExt: map[string]string{
"background_url": "https://cdn.example.com/bg.png",
},
Version: 4,
},
Background: &roomv1.RoomBackgroundImage{
BackgroundId: req.GetBackgroundId(),
RoomId: req.GetMeta().GetRoomId(),
ImageUrl: "https://cdn.example.com/bg.png",
CreatedByUserId: req.GetMeta().GetActorUserId(),
CreatedAtMs: 1_700_000_000_003,
UpdatedAtMs: 1_700_000_000_003,
},
}, nil
}
func (f *fakeRoomClient) JoinRoom(_ context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
f.lastJoin = req
if f.joinErr != nil {
@ -293,27 +336,30 @@ type fakeUserIdentityClient struct {
}
type fakeRoomQueryClient struct {
lastList *roomv1.ListRoomsRequest
lastFeeds *roomv1.ListRoomFeedsRequest
lastMyRoom *roomv1.GetMyRoomRequest
lastCurrent *roomv1.GetCurrentRoomRequest
lastSnapshot *roomv1.GetRoomSnapshotRequest
lastTreasure *roomv1.GetRoomTreasureRequest
lastOnline *roomv1.ListRoomOnlineUsersRequest
resp *roomv1.ListRoomsResponse
feedsResp *roomv1.ListRoomsResponse
myRoomResp *roomv1.GetMyRoomResponse
currentResp *roomv1.GetCurrentRoomResponse
snapshotResp *roomv1.GetRoomSnapshotResponse
treasureResp *roomv1.GetRoomTreasureResponse
onlineResp *roomv1.ListRoomOnlineUsersResponse
err error
feedsErr error
myRoomErr error
currentErr error
snapshotErr error
treasureErr error
onlineErr error
lastList *roomv1.ListRoomsRequest
lastFeeds *roomv1.ListRoomFeedsRequest
lastMyRoom *roomv1.GetMyRoomRequest
lastCurrent *roomv1.GetCurrentRoomRequest
lastSnapshot *roomv1.GetRoomSnapshotRequest
lastBackgrounds *roomv1.ListRoomBackgroundsRequest
lastTreasure *roomv1.GetRoomTreasureRequest
lastOnline *roomv1.ListRoomOnlineUsersRequest
resp *roomv1.ListRoomsResponse
feedsResp *roomv1.ListRoomsResponse
myRoomResp *roomv1.GetMyRoomResponse
currentResp *roomv1.GetCurrentRoomResponse
snapshotResp *roomv1.GetRoomSnapshotResponse
backgroundsResp *roomv1.ListRoomBackgroundsResponse
treasureResp *roomv1.GetRoomTreasureResponse
onlineResp *roomv1.ListRoomOnlineUsersResponse
err error
feedsErr error
myRoomErr error
currentErr error
snapshotErr error
backgroundsErr error
treasureErr error
onlineErr error
}
type fakeUserDeviceClient struct {
@ -761,6 +807,17 @@ func (f *fakeRoomQueryClient) GetRoomSnapshot(_ context.Context, req *roomv1.Get
return &roomv1.GetRoomSnapshotResponse{}, nil
}
func (f *fakeRoomQueryClient) ListRoomBackgrounds(_ context.Context, req *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) {
f.lastBackgrounds = req
if f.backgroundsErr != nil {
return nil, f.backgroundsErr
}
if f.backgroundsResp != nil {
return f.backgroundsResp, nil
}
return &roomv1.ListRoomBackgroundsResponse{}, nil
}
func (f *fakeRoomQueryClient) GetRoomTreasure(_ context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error) {
f.lastTreasure = req
if f.treasureErr != nil {
@ -4679,6 +4736,74 @@ func TestUpdateRoomProfileRPCErrorWritesEnvelope(t *testing.T) {
assertEnvelope(t, recorder, http.StatusConflict, string(xerr.Conflict), "req-profile-conflict")
}
func TestRoomBackgroundRoutesForwardOwnerRequests(t *testing.T) {
client := &fakeRoomClient{}
queryClient := &fakeRoomQueryClient{backgroundsResp: &roomv1.ListRoomBackgroundsResponse{
Backgrounds: []*roomv1.RoomBackgroundImage{{
BackgroundId: 101,
RoomId: "room-1",
ImageUrl: "https://cdn.example.com/bg.png",
CreatedByUserId: 42,
CreatedAtMs: 1_700_000_000_003,
UpdatedAtMs: 1_700_000_000_003,
}},
SelectedBackgroundUrl: "https://cdn.example.com/bg.png",
ServerTimeMs: 1_700_000_000_004,
}}
handler := NewHandler(client)
handler.SetRoomQueryClient(queryClient)
router := handler.Routes(auth.NewVerifier("secret"))
saveReq := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/backgrounds/save", bytes.NewReader([]byte(`{"room_id":"room-1","image_url":" https://cdn.example.com/bg.png "}`)))
saveReq.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
saveReq.Header.Set("X-Request-ID", "req-bg-save")
saveRecorder := httptest.NewRecorder()
router.ServeHTTP(saveRecorder, saveReq)
if saveRecorder.Code != http.StatusOK {
t.Fatalf("save status mismatch: got %d body=%s", saveRecorder.Code, saveRecorder.Body.String())
}
if client.lastSaveBackground == nil || client.lastSaveBackground.GetRoomId() != "room-1" || client.lastSaveBackground.GetImageUrl() != "https://cdn.example.com/bg.png" || client.lastSaveBackground.GetMeta().GetActorUserId() != 42 {
t.Fatalf("SaveRoomBackground forwarding mismatch: %+v", client.lastSaveBackground)
}
listReq := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/room-1/backgrounds", nil)
listReq.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
listReq.Header.Set("X-Request-ID", "req-bg-list")
listRecorder := httptest.NewRecorder()
router.ServeHTTP(listRecorder, listReq)
if listRecorder.Code != http.StatusOK {
t.Fatalf("list status mismatch: got %d body=%s", listRecorder.Code, listRecorder.Body.String())
}
if queryClient.lastBackgrounds == nil || queryClient.lastBackgrounds.GetRoomId() != "room-1" || queryClient.lastBackgrounds.GetViewerUserId() != 42 {
t.Fatalf("ListRoomBackgrounds forwarding mismatch: %+v", queryClient.lastBackgrounds)
}
setReq := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/backgrounds/set", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-bg-set","background_id":"101"}`)))
setReq.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
setReq.Header.Set("X-Request-ID", "req-bg-set")
setRecorder := httptest.NewRecorder()
router.ServeHTTP(setRecorder, setReq)
if setRecorder.Code != http.StatusOK {
t.Fatalf("set status mismatch: got %d body=%s", setRecorder.Code, setRecorder.Body.String())
}
if client.lastSetBackground == nil || client.lastSetBackground.GetMeta().GetRoomId() != "room-1" || client.lastSetBackground.GetMeta().GetCommandId() != "cmd-bg-set" || client.lastSetBackground.GetBackgroundId() != 101 {
t.Fatalf("SetRoomBackground forwarding mismatch: %+v", client.lastSetBackground)
}
var envelope httpkit.ResponseEnvelope
if err := json.NewDecoder(setRecorder.Body).Decode(&envelope); err != nil {
t.Fatalf("decode set response failed: %v", err)
}
data, ok := envelope.Data.(map[string]any)
if !ok {
t.Fatalf("set response data mismatch: %+v", envelope.Data)
}
room, ok := data["room"].(map[string]any)
if !ok || room["background_url"] != "https://cdn.example.com/bg.png" {
t.Fatalf("set response must expose selected background_url: %+v", data["room"])
}
}
func TestCreateRoomRejectsMissingRequiredFieldsBeforeGRPC(t *testing.T) {
tests := []struct {
name string

View File

@ -56,6 +56,9 @@ func (h *Handler) RoomHandlers() httproutes.RoomHandlers {
FollowRoom: h.handleRoomFollow,
CreateRoom: h.createRoom,
UpdateRoomProfile: h.updateRoomProfile,
SaveRoomBackground: h.saveRoomBackground,
ListRoomBackgrounds: h.listRoomBackgrounds,
SetRoomBackground: h.setRoomBackground,
JoinRoom: h.joinRoom,
RoomHeartbeat: h.roomHeartbeat,
LeaveRoom: h.leaveRoom,

View File

@ -705,6 +705,76 @@ func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Re
httpkit.WriteOK(writer, request, updateRoomProfileDataFromProto(resp))
}
// 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"`
}
if !httpkit.Decode(writer, request, &body) {
return
}
body.RoomID = strings.TrimSpace(body.RoomID)
body.ImageURL = strings.TrimSpace(body.ImageURL)
if !roomid.ValidStringID(body.RoomID) || body.ImageURL == "" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.roomClient.SaveRoomBackground(request.Context(), &roomv1.SaveRoomBackgroundRequest{
Meta: httpkit.RoomMeta(request, body.RoomID, ""),
RoomId: body.RoomID,
ImageUrl: body.ImageURL,
})
httpkit.Write(writer, request, saveRoomBackgroundDataFromProto(resp), err)
}
// listRoomBackgrounds 返回房主保存过的背景图素材,以及 Room Cell 快照里的当前生效背景。
func (h *Handler) listRoomBackgrounds(writer http.ResponseWriter, request *http.Request) {
if h.roomQueryClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
roomID := strings.TrimSpace(request.PathValue("room_id"))
if !roomid.ValidStringID(roomID) {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.roomQueryClient.ListRoomBackgrounds(request.Context(), &roomv1.ListRoomBackgroundsRequest{
Meta: httpkit.RoomMeta(request, roomID, ""),
RoomId: roomID,
ViewerUserId: auth.UserIDFromContext(request.Context()),
})
httpkit.Write(writer, request, listRoomBackgroundsDataFromProto(resp), err)
}
// setRoomBackground 把已保存素材设为当前房间背景;权限和状态写入仍由 Room Cell 串行裁决。
func (h *Handler) setRoomBackground(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
BackgroundID flexibleInt64 `json:"background_id"`
}
if !httpkit.Decode(writer, request, &body) {
return
}
body.RoomID = strings.TrimSpace(body.RoomID)
body.CommandID = strings.TrimSpace(body.CommandID)
if !roomid.ValidStringID(body.RoomID) || body.CommandID == "" || int64(body.BackgroundID) <= 0 {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.roomClient.SetRoomBackground(request.Context(), &roomv1.SetRoomBackgroundRequest{
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
BackgroundId: int64(body.BackgroundID),
})
httpkit.Write(writer, request, setRoomBackgroundDataFromProto(resp), err)
}
func trimOptionalString(value *string) {
if value == nil {
return

View File

@ -220,6 +220,33 @@ type updateRoomProfileData struct {
ServerTimeMS int64 `json:"server_time_ms"`
}
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"`
}
type saveRoomBackgroundData struct {
Background roomBackgroundData `json:"background"`
ServerTimeMS int64 `json:"server_time_ms"`
}
type listRoomBackgroundsData struct {
Backgrounds []roomBackgroundData `json:"backgrounds"`
SelectedBackgroundURL string `json:"selected_background_url,omitempty"`
ServerTimeMS int64 `json:"server_time_ms"`
}
type setRoomBackgroundData struct {
Result roomCommandResultData `json:"result"`
Room roomInitialData `json:"room"`
Background roomBackgroundData `json:"background"`
ServerTimeMS int64 `json:"server_time_ms"`
}
type roomSnapshotData struct {
Room roomInitialData `json:"room"`
Seats []roomSeatData `json:"seats"`
@ -247,6 +274,7 @@ type roomInitialData struct {
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"`
@ -478,6 +506,58 @@ func updateRoomProfileDataFromProto(resp *roomv1.UpdateRoomProfileResponse) upda
}
}
func saveRoomBackgroundDataFromProto(resp *roomv1.SaveRoomBackgroundResponse) saveRoomBackgroundData {
if resp == nil {
return saveRoomBackgroundData{}
}
return saveRoomBackgroundData{
Background: roomBackgroundDataFromProto(resp.GetBackground()),
ServerTimeMS: resp.GetServerTimeMs(),
}
}
func listRoomBackgroundsDataFromProto(resp *roomv1.ListRoomBackgroundsResponse) listRoomBackgroundsData {
if resp == nil {
return listRoomBackgroundsData{}
}
backgrounds := make([]roomBackgroundData, 0, len(resp.GetBackgrounds()))
for _, background := range resp.GetBackgrounds() {
backgrounds = append(backgrounds, roomBackgroundDataFromProto(background))
}
return listRoomBackgroundsData{
Backgrounds: backgrounds,
SelectedBackgroundURL: resp.GetSelectedBackgroundUrl(),
ServerTimeMS: resp.GetServerTimeMs(),
}
}
func setRoomBackgroundDataFromProto(resp *roomv1.SetRoomBackgroundResponse) setRoomBackgroundData {
if resp == nil {
return setRoomBackgroundData{}
}
snapshot := resp.GetRoom()
return setRoomBackgroundData{
Result: commandResultDataFromProto(resp.GetResult()),
Room: roomInitialRoomDataFromSnapshot(snapshot, snapshot.GetRoomId()),
Background: roomBackgroundDataFromProto(resp.GetBackground()),
ServerTimeMS: resp.GetResult().GetServerTimeMs(),
}
}
func roomBackgroundDataFromProto(background *roomv1.RoomBackgroundImage) roomBackgroundData {
if background == nil {
return roomBackgroundData{}
}
return roomBackgroundData{
BackgroundID: background.GetBackgroundId(),
RoomID: background.GetRoomId(),
ImageURL: background.GetImageUrl(),
CreatedByUserID: formatOptionalUserID(background.GetCreatedByUserId()),
CreatedAtMS: background.GetCreatedAtMs(),
UpdatedAtMS: background.GetUpdatedAtMs(),
}
}
func roomSnapshotDataFromProto(resp *roomv1.GetRoomSnapshotResponse) roomSnapshotData {
if resp == nil {
return roomSnapshotData{}
@ -511,6 +591,7 @@ func roomInitialRoomDataFromSnapshot(snapshot *roomv1.RoomSnapshot, roomID strin
RoomShortID: roomShortID,
Title: ext["title"],
CoverURL: ext["cover_url"],
BackgroundURL: ext["background_url"],
Description: ext["description"],
OwnerUserID: formatOptionalUserID(snapshot.GetOwnerUserId()),
Mode: snapshot.GetMode(),

View File

@ -88,6 +88,19 @@ CREATE TABLE IF NOT EXISTS room_user_gift_stats (
KEY idx_room_user_gift_stats_room_value (app_code, room_id, gift_value DESC, last_gift_at_ms DESC, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='房间用户送礼价值统计表';
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(1024) NOT NULL COMMENT '背景图 URL',
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),
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_snapshots (
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',

View File

@ -181,10 +181,23 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
}
base.ActorUserID = body.GetActorUserId()
base.Attributes = map[string]string{
"room_name": body.GetRoomName(),
"room_avatar": body.GetRoomAvatar(),
"room_description": body.GetRoomDescription(),
"seat_count": fmt.Sprintf("%d", body.GetSeatCount()),
"room_name": body.GetRoomName(),
"room_avatar": body.GetRoomAvatar(),
"room_description": body.GetRoomDescription(),
"seat_count": fmt.Sprintf("%d", body.GetSeatCount()),
"room_background_url": body.GetRoomBackgroundUrl(),
}
return base, true, nil
case "RoomBackgroundChanged":
// 背景切换是独立的房间 UI 状态变更,客户端无需从通用资料更新里反推。
var body roomeventsv1.RoomBackgroundChanged
if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil {
return tencentim.RoomEvent{}, false, err
}
base.ActorUserID = body.GetActorUserId()
base.Attributes = map[string]string{
"background_id": fmt.Sprintf("%d", body.GetBackgroundId()),
"room_background_url": body.GetRoomBackgroundUrl(),
}
return base, true, nil
case "RoomChatEnabledChanged":
@ -358,6 +371,8 @@ func eventTypeForClient(eventType string) string {
return "room_mic_seat_locked"
case "RoomProfileUpdated":
return "room_profile_updated"
case "RoomBackgroundChanged":
return "room_background_changed"
case "RoomChatEnabledChanged":
return "room_chat_enabled_changed"
case "RoomPasswordChanged":

View File

@ -32,6 +32,28 @@ func TestRoomEventFromEnvelopeSkipsNonIMEvents(t *testing.T) {
}
}
func TestRoomBackgroundChangedPublishesDedicatedIMEvent(t *testing.T) {
record, err := outbox.Build("room-bg", "RoomBackgroundChanged", 7, time.Now(), &roomeventsv1.RoomBackgroundChanged{
ActorUserId: 42,
BackgroundId: 101,
RoomBackgroundUrl: "https://cdn.example.com/bg.png",
})
if err != nil {
t.Fatalf("Build RoomBackgroundChanged envelope failed: %v", err)
}
event, publish, err := roomEventFromEnvelope(record.Envelope)
if err != nil {
t.Fatalf("RoomBackgroundChanged should decode: %v", err)
}
if !publish {
t.Fatal("RoomBackgroundChanged must publish Tencent IM message")
}
if event.EventType != "room_background_changed" || event.Attributes["room_background_url"] != "https://cdn.example.com/bg.png" || event.Attributes["background_id"] != "101" {
t.Fatalf("background IM event mismatch: %+v", event)
}
}
func TestTencentIMPublisherRemovesGroupMemberBeforeKickMessage(t *testing.T) {
paths := make([]string, 0, 2)
client, err := tencentim.NewRESTClient(tencentim.RESTConfig{

View File

@ -93,6 +93,17 @@ type UpdateRoomProfile struct {
// Type 返回命令类型。
func (UpdateRoomProfile) Type() string { return "update_room_profile" }
// SetRoomBackground 定义房主把已保存背景图设置为当前房间背景的请求。
type SetRoomBackground struct {
Base
// BackgroundID 必须归属于当前房间,避免房主把其他房间的素材串用到自己的房间状态。
BackgroundID int64 `json:"background_id"`
// BackgroundURL 是提交前从房间背景素材表解析出的确定性快照,恢复时不再回查列表表。
BackgroundURL string `json:"background_url"`
}
func (SetRoomBackground) Type() string { return "set_room_background" }
// JoinRoom 定义业务进房请求。
type JoinRoom struct {
Base
@ -502,6 +513,8 @@ func Deserialize(commandType string, payload []byte) (Command, error) {
cmd = &CreateRoom{}
case UpdateRoomProfile{}.Type():
cmd = &UpdateRoomProfile{}
case SetRoomBackground{}.Type():
cmd = &SetRoomBackground{}
case JoinRoom{}.Type():
cmd = &JoinRoom{}
case RoomHeartbeat{}.Type():

View File

@ -0,0 +1,251 @@
package service
import (
"context"
"strings"
"time"
"unicode/utf8"
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/roomid"
"hyapp/pkg/tencentim"
"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"
)
// SaveRoomBackground 把房主上传后的背景图 URL 保存到房间维度素材列表。
// 该动作只维护低频素材表,不改变 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)
if err != nil {
return nil, err
}
if err := s.requireRoomOwnerMeta(ctx, roomID, actorUserID); err != nil {
return nil, err
}
nowMS := s.clock.Now().UnixMilli()
background, err := s.repository.SaveRoomBackground(ctx, RoomBackgroundImage{
AppCode: req.GetMeta().GetAppCode(),
RoomID: roomID,
ImageURL: imageURL,
CreatedByUserID: actorUserID,
CreatedAtMS: nowMS,
UpdatedAtMS: nowMS,
})
if err != nil {
return nil, err
}
return &roomv1.SaveRoomBackgroundResponse{
Background: roomBackgroundToProto(background),
ServerTimeMs: s.clock.Now().UnixMilli(),
}, nil
}
// ListRoomBackgrounds 返回房主保存过的背景图列表和当前生效背景。
// 列表属于房间编辑面板数据,因此只允许房主读取。
func (s *Service) ListRoomBackgrounds(ctx context.Context, req *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) {
ctx = contextFromMeta(ctx, req.GetMeta())
roomID, viewerUserID, err := normalizeListRoomBackgrounds(req)
if err != nil {
return nil, err
}
if err := s.requireRoomOwnerMeta(ctx, roomID, viewerUserID); err != nil {
return nil, err
}
backgrounds, err := s.repository.ListRoomBackgrounds(ctx, roomID)
if err != nil {
return nil, err
}
snapshot, err := s.currentSnapshot(ctx, roomID)
if err != nil {
return nil, err
}
return &roomv1.ListRoomBackgroundsResponse{
Backgrounds: roomBackgroundsToProto(backgrounds),
SelectedBackgroundUrl: snapshot.GetRoomExt()[roomExtBackgroundKey],
ServerTimeMs: s.clock.Now().UnixMilli(),
}, nil
}
// SetRoomBackground 把已保存的背景图设置为当前房间背景。
// 当前背景属于房间展示状态,必须经 Room Cell 命令链路、command log 和快照恢复。
func (s *Service) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBackgroundRequest) (*roomv1.SetRoomBackgroundResponse, error) {
ctx = contextFromMeta(ctx, req.GetMeta())
roomID, actorUserID, backgroundID, err := normalizeSetRoomBackground(req)
if err != nil {
return nil, err
}
if err := s.requireRoomOwnerMeta(ctx, roomID, actorUserID); err != nil {
return nil, err
}
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 := command.SetRoomBackground{
Base: baseFromMeta(req.GetMeta()),
BackgroundID: background.BackgroundID,
BackgroundURL: background.ImageURL,
}
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 current.RoomExt == nil {
current.RoomExt = make(map[string]string)
}
if current.RoomExt[roomExtBackgroundKey] == cmd.BackgroundURL {
return mutationResult{snapshot: current.ToProto()}, nil, nil
}
current.RoomExt[roomExtBackgroundKey] = cmd.BackgroundURL
current.Version++
backgroundEvent, err := outbox.Build(current.RoomID, "RoomBackgroundChanged", current.Version, now, &roomeventsv1.RoomBackgroundChanged{
ActorUserId: cmd.ActorUserID(),
BackgroundId: cmd.BackgroundID,
RoomBackgroundUrl: current.RoomExt[roomExtBackgroundKey],
})
if err != nil {
return mutationResult{}, nil, err
}
return mutationResult{
snapshot: current.ToProto(),
syncEvent: &tencentim.RoomEvent{
EventID: backgroundEvent.EventID,
RoomID: current.RoomID,
EventType: "room_background_changed",
ActorUserID: cmd.ActorUserID(),
RoomVersion: current.Version,
Attributes: map[string]string{
"background_id": int64String(cmd.BackgroundID),
"room_background_url": current.RoomExt[roomExtBackgroundKey],
},
},
}, []outbox.Record{backgroundEvent}, nil
})
if err != nil {
return nil, err
}
return &roomv1.SetRoomBackgroundResponse{
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
Room: result.snapshot,
Background: roomBackgroundToProto(background),
}, nil
}
func normalizeSaveRoomBackground(req *roomv1.SaveRoomBackgroundRequest) (string, int64, string, error) {
if req == nil || req.GetMeta() == nil {
return "", 0, "", 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")
}
actorUserID := req.GetMeta().GetActorUserId()
if actorUserID <= 0 {
return "", 0, "", 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")
}
if utf8.RuneCountInString(imageURL) > maxRoomBackgroundRunes {
return "", 0, "", xerr.New(xerr.InvalidArgument, "image_url is too long")
}
return roomID, actorUserID, imageURL, nil
}
func normalizeListRoomBackgrounds(req *roomv1.ListRoomBackgroundsRequest) (string, int64, error) {
if req == nil || req.GetMeta() == nil {
return "", 0, xerr.New(xerr.InvalidArgument, "request is required")
}
roomID := strings.TrimSpace(req.GetRoomId())
if !roomid.ValidStringID(roomID) {
return "", 0, xerr.New(xerr.InvalidArgument, "room_id is invalid")
}
viewerUserID := req.GetViewerUserId()
if viewerUserID <= 0 {
return "", 0, xerr.New(xerr.InvalidArgument, "viewer_user_id is required")
}
return roomID, viewerUserID, nil
}
func normalizeSetRoomBackground(req *roomv1.SetRoomBackgroundRequest) (string, int64, int64, error) {
if req == nil || req.GetMeta() == nil {
return "", 0, 0, xerr.New(xerr.InvalidArgument, "request is required")
}
roomID := strings.TrimSpace(req.GetMeta().GetRoomId())
if !roomid.ValidStringID(roomID) {
return "", 0, 0, xerr.New(xerr.InvalidArgument, "room_id is invalid")
}
actorUserID := req.GetMeta().GetActorUserId()
if actorUserID <= 0 {
return "", 0, 0, xerr.New(xerr.InvalidArgument, "actor_user_id is required")
}
if strings.TrimSpace(req.GetMeta().GetCommandId()) == "" {
return "", 0, 0, xerr.New(xerr.InvalidArgument, "command_id is required")
}
if req.GetBackgroundId() <= 0 {
return "", 0, 0, xerr.New(xerr.InvalidArgument, "background_id is required")
}
return roomID, actorUserID, req.GetBackgroundId(), nil
}
func (s *Service) requireRoomOwnerMeta(ctx context.Context, roomID string, actorUserID int64) error {
meta, exists, err := s.repository.GetRoomMeta(ctx, roomID)
if err != nil {
return err
}
if !exists {
return xerr.New(xerr.NotFound, "room not found")
}
if meta.OwnerUserID != actorUserID {
return xerr.New(xerr.PermissionDenied, "permission denied")
}
if meta.Status != state.RoomStatusActive {
return xerr.New(xerr.Conflict, "room is not active")
}
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
}

View File

@ -0,0 +1,135 @@
package service_test
import (
"context"
"testing"
"time"
roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/integration"
roomservice "hyapp/services/room-service/internal/room/service"
"hyapp/services/room-service/internal/router"
"hyapp/services/room-service/internal/testutil/mysqltest"
)
func TestRoomBackgroundSaveListSetFlow(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := roomservice.New(roomservice.Config{
NodeID: "node-room-background-test",
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 1,
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
roomID := "room-background-flow"
ownerID := int64(7101)
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, ownerID),
SeatCount: 10,
Mode: "voice",
VisibleRegionId: 1001,
RoomName: "Background Flow",
RoomShortId: "bg-flow",
}); err != nil {
t.Fatalf("create room failed: %v", err)
}
saveResp, err := svc.SaveRoomBackground(ctx, &roomv1.SaveRoomBackgroundRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID, ActorUserId: ownerID},
RoomId: roomID,
ImageUrl: " https://cdn.example.com/room-bg.png ",
})
if err != nil {
t.Fatalf("save background failed: %v", err)
}
if saveResp.GetBackground().GetBackgroundId() <= 0 || saveResp.GetBackground().GetImageUrl() != "https://cdn.example.com/room-bg.png" {
t.Fatalf("save response mismatch: %+v", saveResp.GetBackground())
}
listResp, err := svc.ListRoomBackgrounds(ctx, &roomv1.ListRoomBackgroundsRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
RoomId: roomID,
ViewerUserId: ownerID,
})
if err != nil {
t.Fatalf("list backgrounds failed: %v", err)
}
if len(listResp.GetBackgrounds()) != 1 || listResp.GetSelectedBackgroundUrl() != "" {
t.Fatalf("list response before set mismatch: %+v", listResp)
}
setResp, err := svc.SetRoomBackground(ctx, &roomv1.SetRoomBackgroundRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID, ActorUserId: ownerID, CommandId: "cmd-room-bg-set"},
BackgroundId: saveResp.GetBackground().GetBackgroundId(),
})
if err != nil {
t.Fatalf("set background failed: %v", err)
}
if !setResp.GetResult().GetApplied() || setResp.GetRoom().GetRoomExt()["background_url"] != "https://cdn.example.com/room-bg.png" {
t.Fatalf("set response mismatch: %+v", setResp)
}
snapshotResp, err := svc.GetRoomSnapshot(ctx, &roomv1.GetRoomSnapshotRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
RoomId: roomID,
ViewerUserId: ownerID,
})
if err != nil {
t.Fatalf("snapshot after set failed: %v", err)
}
if snapshotResp.GetRoom().GetRoomExt()["background_url"] != "https://cdn.example.com/room-bg.png" {
t.Fatalf("snapshot must expose selected background_url: %+v", snapshotResp.GetRoom().GetRoomExt())
}
}
func TestRoomBackgroundOnlyOwnerCanEdit(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
svc := roomservice.New(roomservice.Config{
NodeID: "node-room-background-owner-test",
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 1,
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
roomID := "room-background-owner"
ownerID := int64(7201)
otherID := int64(7202)
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, ownerID),
SeatCount: 10,
Mode: "voice",
VisibleRegionId: 1001,
RoomName: "Background Owner",
RoomShortId: "bg-owner",
}); err != nil {
t.Fatalf("create room failed: %v", err)
}
if _, err := svc.SaveRoomBackground(ctx, &roomv1.SaveRoomBackgroundRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID, ActorUserId: otherID},
RoomId: roomID,
ImageUrl: "https://cdn.example.com/other-bg.png",
}); !xerr.IsCode(err, xerr.PermissionDenied) {
t.Fatalf("non-owner save must be denied: %v", err)
}
saveResp, err := svc.SaveRoomBackground(ctx, &roomv1.SaveRoomBackgroundRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID, ActorUserId: ownerID},
RoomId: roomID,
ImageUrl: "https://cdn.example.com/owner-bg.png",
})
if err != nil {
t.Fatalf("owner save background failed: %v", err)
}
if _, err := svc.SetRoomBackground(ctx, &roomv1.SetRoomBackgroundRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID, ActorUserId: otherID, CommandId: "cmd-room-bg-other"},
BackgroundId: saveResp.GetBackground().GetBackgroundId(),
}); !xerr.IsCode(err, xerr.PermissionDenied) {
t.Fatalf("non-owner set must be denied: %v", err)
}
}

View File

@ -25,6 +25,7 @@ const (
roomExtCoverURLKey = "cover_url"
roomExtDescriptionKey = "description"
roomExtShortIDKey = "room_short_id"
roomExtBackgroundKey = "background_url"
// defaultRoomAvatar 是房间头像缺省值,语义上代表系统默认资源,不绑定外部 CDN 地址。
defaultRoomAvatar = "hyapp://room/default-avatar"
@ -33,6 +34,7 @@ const (
maxRoomNameRunes = 128
maxRoomAvatarRunes = 512
maxRoomDescriptionRunes = 512
maxRoomBackgroundRunes = 1024
)
// roomProfileInput 是创建房间时需要落到 RoomExt 的展示资料集合。

View File

@ -52,11 +52,12 @@ func (s *Service) UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomP
current.Version++
seatCount := int32(len(current.MicSeats))
profileEvent, err := outbox.Build(current.RoomID, "RoomProfileUpdated", current.Version, now, &roomeventsv1.RoomProfileUpdated{
ActorUserId: cmd.ActorUserID(),
RoomName: current.RoomExt[roomExtTitleKey],
RoomAvatar: current.RoomExt[roomExtCoverURLKey],
RoomDescription: current.RoomExt[roomExtDescriptionKey],
SeatCount: seatCount,
ActorUserId: cmd.ActorUserID(),
RoomName: current.RoomExt[roomExtTitleKey],
RoomAvatar: current.RoomExt[roomExtCoverURLKey],
RoomDescription: current.RoomExt[roomExtDescriptionKey],
SeatCount: seatCount,
RoomBackgroundUrl: current.RoomExt[roomExtBackgroundKey],
})
if err != nil {
return mutationResult{}, nil, err
@ -71,10 +72,11 @@ func (s *Service) UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomP
ActorUserID: cmd.ActorUserID(),
RoomVersion: current.Version,
Attributes: map[string]string{
"room_name": current.RoomExt[roomExtTitleKey],
"room_avatar": current.RoomExt[roomExtCoverURLKey],
"room_description": current.RoomExt[roomExtDescriptionKey],
"seat_count": int32String(seatCount),
"room_name": current.RoomExt[roomExtTitleKey],
"room_avatar": current.RoomExt[roomExtCoverURLKey],
"room_description": current.RoomExt[roomExtDescriptionKey],
"seat_count": int32String(seatCount),
"room_background_url": current.RoomExt[roomExtBackgroundKey],
},
},
}

View File

@ -139,6 +139,12 @@ func replay(current *state.RoomState, cmd command.Command) error {
return err
}
current.Version++
case *command.SetRoomBackground:
if current.RoomExt == nil {
current.RoomExt = make(map[string]string)
}
current.RoomExt[roomExtBackgroundKey] = typed.BackgroundURL
current.Version++
case *command.JoinRoom:
if existing, exists := current.OnlineUsers[typed.ActorUserID()]; exists {
// 重复 JoinRoom 回放只刷新 last_seen不能重置首次进房时间或降级 owner 角色。

View File

@ -92,6 +92,18 @@ type RoomCloseInfo struct {
ClearOnOpen bool
}
// RoomBackgroundImage 是房间 owner 保存过的背景图素材。
// 当前生效背景仍属于 Room Cell 快照;该表只保存可选素材列表。
type RoomBackgroundImage struct {
AppCode string
BackgroundID int64
RoomID string
ImageURL string
CreatedByUserID int64
CreatedAtMS int64
UpdatedAtMS int64
}
// SnapshotRecord 对应 room_snapshots 中的一条最新快照。
type SnapshotRecord struct {
// AppCode 是快照所属 App恢复时必须和房间 meta 保持一致。
@ -480,6 +492,12 @@ type Repository interface {
GetRoomMeta(ctx context.Context, roomID string) (RoomMeta, bool, error)
// GetRoomMetaByOwner 查询 owner 已创建的房间;产品规则要求一个用户只能创建一个房间。
GetRoomMetaByOwner(ctx context.Context, ownerUserID int64) (RoomMeta, bool, error)
// SaveRoomBackground 保存房间 owner 上传过的背景图 URL相同房间相同 URL 幂等返回同一条素材。
SaveRoomBackground(ctx context.Context, background RoomBackgroundImage) (RoomBackgroundImage, error)
// GetRoomBackground 精确读取一个背景图素材,用于 SetRoomBackground 校验归属。
GetRoomBackground(ctx context.Context, roomID string, backgroundID int64) (RoomBackgroundImage, bool, error)
// ListRoomBackgrounds 按房间读取背景图素材列表,不读取 Room Cell 状态。
ListRoomBackgrounds(ctx context.Context, roomID string) ([]RoomBackgroundImage, error)
// GetCommand 读取已提交命令,用于幂等重试和 command_id 冲突判定。
GetCommand(ctx context.Context, roomID string, commandID string) (CommandRecord, bool, error)
// SaveCommand 追加成功命令日志,关键命令恢复必须依赖它。

View File

@ -147,6 +147,18 @@ func (r *Repository) Migrate(ctx context.Context) error {
PRIMARY KEY (app_code, room_id, user_id),
KEY idx_room_user_gift_stats_room_value (app_code, room_id, gift_value DESC, last_gift_at_ms DESC, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_background_images (
app_code VARCHAR(32) NOT NULL,
background_id BIGINT NOT NULL AUTO_INCREMENT,
room_id VARCHAR(64) NOT NULL,
image_url VARCHAR(1024) NOT NULL,
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),
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_snapshots (
app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) NOT NULL,
@ -517,6 +529,92 @@ func (r *Repository) GetRoomMetaByOwner(ctx context.Context, ownerUserID int64)
return meta, true, nil
}
// SaveRoomBackground 保存房间背景图素材;相同房间相同 URL 按幂等返回原记录。
func (r *Repository) SaveRoomBackground(ctx context.Context, background roomservice.RoomBackgroundImage) (roomservice.RoomBackgroundImage, error) {
appCode := normalizedRecordAppCode(ctx, background.AppCode)
roomID := strings.TrimSpace(background.RoomID)
imageURL := strings.TrimSpace(background.ImageURL)
nowMS := background.CreatedAtMS
if nowMS <= 0 {
nowMS = time.Now().UTC().UnixMilli()
}
_, 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)
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)
}
// 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
FROM room_background_images
WHERE app_code = ? AND room_id = ? AND background_id = ?
LIMIT 1
`, appcode.FromContext(ctx), strings.TrimSpace(roomID), backgroundID)
background, err := scanRoomBackground(row)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return roomservice.RoomBackgroundImage{}, false, nil
}
return roomservice.RoomBackgroundImage{}, false, err
}
return background, true, nil
}
// 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
FROM room_background_images
WHERE app_code = ? AND room_id = ?
ORDER BY created_at_ms DESC, background_id DESC
`, appcode.FromContext(ctx), strings.TrimSpace(roomID))
if err != nil {
return nil, err
}
defer rows.Close()
backgrounds := []roomservice.RoomBackgroundImage{}
for rows.Next() {
background, err := scanRoomBackground(rows)
if err != nil {
return nil, err
}
backgrounds = append(backgrounds, background)
}
return backgrounds, rows.Err()
}
func scanRoomBackground(scanner interface{ Scan(dest ...any) error }) (roomservice.RoomBackgroundImage, error) {
var background roomservice.RoomBackgroundImage
err := scanner.Scan(
&background.AppCode,
&background.BackgroundID,
&background.RoomID,
&background.ImageURL,
&background.CreatedByUserID,
&background.CreatedAtMS,
&background.UpdatedAtMS,
)
return background, err
}
// GetCommand 读取已提交命令,用于幂等重试和 command_id 冲突判定。
func (r *Repository) GetCommand(ctx context.Context, roomID string, commandID string) (roomservice.CommandRecord, bool, error) {
// 幂等检查只看 command log只有成功提交过的命令才算 seen。

View File

@ -84,6 +84,21 @@ func (s *Server) UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomPr
return mapServiceResult(s.svc.UpdateRoomProfile(ctx, req))
}
func (s *Server) SaveRoomBackground(ctx context.Context, req *roomv1.SaveRoomBackgroundRequest) (*roomv1.SaveRoomBackgroundResponse, error) {
ctx = contextWithMetaApp(ctx, req.GetMeta())
return mapServiceResult(s.svc.SaveRoomBackground(ctx, req))
}
func (s *Server) SetRoomBackground(ctx context.Context, req *roomv1.SetRoomBackgroundRequest) (*roomv1.SetRoomBackgroundResponse, error) {
ctx = contextWithMetaApp(ctx, req.GetMeta())
if resp, forwarded, err := forwardCommand(s, ctx, req.GetMeta(), func(callCtx context.Context, client roomv1.RoomCommandServiceClient) (*roomv1.SetRoomBackgroundResponse, error) {
return client.SetRoomBackground(callCtx, req)
}); forwarded {
return resp, err
}
return mapServiceResult(s.svc.SetRoomBackground(ctx, req))
}
// JoinRoom 代理到领域服务。
func (s *Server) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
// JoinRoom 只维护 room-service presence真实消息连接由腾讯云 IM SDK 处理。
@ -463,6 +478,16 @@ func (s *Server) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapsho
return mapServiceResult(s.svc.GetRoomSnapshot(ctx, req))
}
func (s *Server) ListRoomBackgrounds(ctx context.Context, req *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) {
ctx = contextWithMetaApp(ctx, req.GetMeta())
if resp, forwarded, err := forwardQuery(s, ctx, req.GetMeta().GetAppCode(), req.GetRoomId(), func(callCtx context.Context, client roomv1.RoomQueryServiceClient) (*roomv1.ListRoomBackgroundsResponse, error) {
return client.ListRoomBackgrounds(callCtx, req)
}); forwarded {
return resp, err
}
return mapServiceResult(s.svc.ListRoomBackgrounds(ctx, req))
}
// GetRoomTreasure 代理到房间宝箱只读查询。
func (s *Server) GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error) {
// 宝箱查询不刷新 presence当前进度仍以 Room Cell 快照和 UTC 日边界为准。