Merge branch 'test'

This commit is contained in:
zhx 2026-05-29 12:00:28 +08:00
commit 0ecb394dfc
41 changed files with 4304 additions and 712 deletions

View File

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

View File

@ -34,6 +34,14 @@ message RoomProfileUpdated {
string room_avatar = 3; string room_avatar = 3;
string room_description = 4; string room_description = 4;
int32 seat_count = 5; 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 // RoomUserJoined

File diff suppressed because it is too large Load Diff

View File

@ -299,6 +299,15 @@ message AdminCancelRoomPinResponse {
int64 server_time_ms = 2; int64 server_time_ms = 2;
} }
// RoomBanState ban
message RoomBanState {
int64 user_id = 1;
int64 actor_user_id = 2;
int64 created_at_ms = 3;
// expires_at_ms 0 ban 0 UTC
int64 expires_at_ms = 4;
}
// RoomSnapshot room-service // RoomSnapshot room-service
message RoomSnapshot { message RoomSnapshot {
string room_id = 1; string room_id = 1;
@ -322,6 +331,53 @@ message RoomSnapshot {
bool locked = 19; bool locked = 19;
// treasure GetRoomTreasure // treasure GetRoomTreasure
RoomTreasureState treasure = 20; RoomTreasureState treasure = 20;
// ban_states ban ban_user_ids
repeated RoomBanState ban_states = 21;
}
// 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 // CreateRoomRequest
@ -662,6 +718,8 @@ message MuteUserResponse {
message KickUserRequest { message KickUserRequest {
RequestMeta meta = 1; RequestMeta meta = 1;
int64 target_user_id = 2; int64 target_user_id = 2;
// duration_ms 0 ban 0 UTC
int64 duration_ms = 3;
} }
// KickUserResponse // KickUserResponse
@ -927,6 +985,34 @@ message ListRoomOnlineUsersResponse {
repeated RoomOnlineUser items = 6; repeated RoomOnlineUser items = 6;
} }
// RoomBannedUser gateway
message RoomBannedUser {
int64 user_id = 1;
int64 actor_user_id = 2;
int64 created_at_ms = 3;
// unban_at_ms 0 ban 0 UTC
int64 unban_at_ms = 4;
int64 remaining_ms = 5;
bool permanent = 6;
}
// ListRoomBannedUsersRequest
message ListRoomBannedUsersRequest {
RequestMeta meta = 1;
string room_id = 2;
int64 viewer_user_id = 3;
int32 page = 4;
int32 page_size = 5;
}
message ListRoomBannedUsersResponse {
repeated RoomBannedUser items = 1;
int64 total = 2;
int32 page = 3;
int32 page_size = 4;
int64 server_time_ms = 5;
}
// FollowRoomRequest // FollowRoomRequest
// room-service - Room Cell command log // room-service - Room Cell command log
message FollowRoomRequest { message FollowRoomRequest {
@ -959,6 +1045,8 @@ message UnfollowRoomResponse {
service RoomCommandService { service RoomCommandService {
rpc CreateRoom(CreateRoomRequest) returns (CreateRoomResponse); rpc CreateRoom(CreateRoomRequest) returns (CreateRoomResponse);
rpc UpdateRoomProfile(UpdateRoomProfileRequest) returns (UpdateRoomProfileResponse); rpc UpdateRoomProfile(UpdateRoomProfileRequest) returns (UpdateRoomProfileResponse);
rpc SaveRoomBackground(SaveRoomBackgroundRequest) returns (SaveRoomBackgroundResponse);
rpc SetRoomBackground(SetRoomBackgroundRequest) returns (SetRoomBackgroundResponse);
rpc JoinRoom(JoinRoomRequest) returns (JoinRoomResponse); rpc JoinRoom(JoinRoomRequest) returns (JoinRoomResponse);
rpc RoomHeartbeat(RoomHeartbeatRequest) returns (RoomHeartbeatResponse); rpc RoomHeartbeat(RoomHeartbeatRequest) returns (RoomHeartbeatResponse);
rpc LeaveRoom(LeaveRoomRequest) returns (LeaveRoomResponse); rpc LeaveRoom(LeaveRoomRequest) returns (LeaveRoomResponse);
@ -1007,6 +1095,8 @@ service RoomQueryService {
rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse); rpc GetMyRoom(GetMyRoomRequest) returns (GetMyRoomResponse);
rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse); rpc GetCurrentRoom(GetCurrentRoomRequest) returns (GetCurrentRoomResponse);
rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse); rpc GetRoomSnapshot(GetRoomSnapshotRequest) returns (GetRoomSnapshotResponse);
rpc ListRoomBackgrounds(ListRoomBackgroundsRequest) returns (ListRoomBackgroundsResponse);
rpc GetRoomTreasure(GetRoomTreasureRequest) returns (GetRoomTreasureResponse); rpc GetRoomTreasure(GetRoomTreasureRequest) returns (GetRoomTreasureResponse);
rpc ListRoomOnlineUsers(ListRoomOnlineUsersRequest) returns (ListRoomOnlineUsersResponse); rpc ListRoomOnlineUsers(ListRoomOnlineUsersRequest) returns (ListRoomOnlineUsersResponse);
rpc ListRoomBannedUsers(ListRoomBannedUsersRequest) returns (ListRoomBannedUsersResponse);
} }

View File

@ -21,6 +21,8 @@ const _ = grpc.SupportPackageIsVersion9
const ( const (
RoomCommandService_CreateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CreateRoom" RoomCommandService_CreateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CreateRoom"
RoomCommandService_UpdateRoomProfile_FullMethodName = "/hyapp.room.v1.RoomCommandService/UpdateRoomProfile" 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_JoinRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/JoinRoom"
RoomCommandService_RoomHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/RoomHeartbeat" RoomCommandService_RoomHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/RoomHeartbeat"
RoomCommandService_LeaveRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/LeaveRoom" RoomCommandService_LeaveRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/LeaveRoom"
@ -58,6 +60,8 @@ const (
type RoomCommandServiceClient interface { type RoomCommandServiceClient interface {
CreateRoom(ctx context.Context, in *CreateRoomRequest, opts ...grpc.CallOption) (*CreateRoomResponse, error) CreateRoom(ctx context.Context, in *CreateRoomRequest, opts ...grpc.CallOption) (*CreateRoomResponse, error)
UpdateRoomProfile(ctx context.Context, in *UpdateRoomProfileRequest, opts ...grpc.CallOption) (*UpdateRoomProfileResponse, 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) JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error)
RoomHeartbeat(ctx context.Context, in *RoomHeartbeatRequest, opts ...grpc.CallOption) (*RoomHeartbeatResponse, error) RoomHeartbeat(ctx context.Context, in *RoomHeartbeatRequest, opts ...grpc.CallOption) (*RoomHeartbeatResponse, error)
LeaveRoom(ctx context.Context, in *LeaveRoomRequest, opts ...grpc.CallOption) (*LeaveRoomResponse, 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 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) { func (c *roomCommandServiceClient) JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(JoinRoomResponse) out := new(JoinRoomResponse)
@ -393,6 +417,8 @@ func (c *roomCommandServiceClient) UnfollowRoom(ctx context.Context, in *Unfollo
type RoomCommandServiceServer interface { type RoomCommandServiceServer interface {
CreateRoom(context.Context, *CreateRoomRequest) (*CreateRoomResponse, error) CreateRoom(context.Context, *CreateRoomRequest) (*CreateRoomResponse, error)
UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, 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) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error)
RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error) RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error)
LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, 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) { func (UnimplementedRoomCommandServiceServer) UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UpdateRoomProfile not implemented") 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) { func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) {
return nil, status.Error(codes.Unimplemented, "method JoinRoom not implemented") 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) 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) { func _RoomCommandService_JoinRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(JoinRoomRequest) in := new(JoinRoomRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -1075,6 +1143,14 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
MethodName: "UpdateRoomProfile", MethodName: "UpdateRoomProfile",
Handler: _RoomCommandService_UpdateRoomProfile_Handler, Handler: _RoomCommandService_UpdateRoomProfile_Handler,
}, },
{
MethodName: "SaveRoomBackground",
Handler: _RoomCommandService_SaveRoomBackground_Handler,
},
{
MethodName: "SetRoomBackground",
Handler: _RoomCommandService_SetRoomBackground_Handler,
},
{ {
MethodName: "JoinRoom", MethodName: "JoinRoom",
Handler: _RoomCommandService_JoinRoom_Handler, Handler: _RoomCommandService_JoinRoom_Handler,
@ -1344,8 +1420,10 @@ const (
RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom" RoomQueryService_GetMyRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetMyRoom"
RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom" RoomQueryService_GetCurrentRoom_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetCurrentRoom"
RoomQueryService_GetRoomSnapshot_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomSnapshot" 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_GetRoomTreasure_FullMethodName = "/hyapp.room.v1.RoomQueryService/GetRoomTreasure"
RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers" RoomQueryService_ListRoomOnlineUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomOnlineUsers"
RoomQueryService_ListRoomBannedUsers_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers"
) )
// RoomQueryServiceClient is the client API for RoomQueryService service. // RoomQueryServiceClient is the client API for RoomQueryService service.
@ -1365,8 +1443,10 @@ type RoomQueryServiceClient interface {
GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error) GetMyRoom(ctx context.Context, in *GetMyRoomRequest, opts ...grpc.CallOption) (*GetMyRoomResponse, error)
GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error) GetCurrentRoom(ctx context.Context, in *GetCurrentRoomRequest, opts ...grpc.CallOption) (*GetCurrentRoomResponse, error)
GetRoomSnapshot(ctx context.Context, in *GetRoomSnapshotRequest, opts ...grpc.CallOption) (*GetRoomSnapshotResponse, 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) GetRoomTreasure(ctx context.Context, in *GetRoomTreasureRequest, opts ...grpc.CallOption) (*GetRoomTreasureResponse, error)
ListRoomOnlineUsers(ctx context.Context, in *ListRoomOnlineUsersRequest, opts ...grpc.CallOption) (*ListRoomOnlineUsersResponse, error) ListRoomOnlineUsers(ctx context.Context, in *ListRoomOnlineUsersRequest, opts ...grpc.CallOption) (*ListRoomOnlineUsersResponse, error)
ListRoomBannedUsers(ctx context.Context, in *ListRoomBannedUsersRequest, opts ...grpc.CallOption) (*ListRoomBannedUsersResponse, error)
} }
type roomQueryServiceClient struct { type roomQueryServiceClient struct {
@ -1487,6 +1567,16 @@ func (c *roomQueryServiceClient) GetRoomSnapshot(ctx context.Context, in *GetRoo
return out, nil 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) { func (c *roomQueryServiceClient) GetRoomTreasure(ctx context.Context, in *GetRoomTreasureRequest, opts ...grpc.CallOption) (*GetRoomTreasureResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetRoomTreasureResponse) out := new(GetRoomTreasureResponse)
@ -1507,6 +1597,16 @@ func (c *roomQueryServiceClient) ListRoomOnlineUsers(ctx context.Context, in *Li
return out, nil return out, nil
} }
func (c *roomQueryServiceClient) ListRoomBannedUsers(ctx context.Context, in *ListRoomBannedUsersRequest, opts ...grpc.CallOption) (*ListRoomBannedUsersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListRoomBannedUsersResponse)
err := c.cc.Invoke(ctx, RoomQueryService_ListRoomBannedUsers_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// RoomQueryServiceServer is the server API for RoomQueryService service. // RoomQueryServiceServer is the server API for RoomQueryService service.
// All implementations must embed UnimplementedRoomQueryServiceServer // All implementations must embed UnimplementedRoomQueryServiceServer
// for forward compatibility. // for forward compatibility.
@ -1524,8 +1624,10 @@ type RoomQueryServiceServer interface {
GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error) GetMyRoom(context.Context, *GetMyRoomRequest) (*GetMyRoomResponse, error)
GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error) GetCurrentRoom(context.Context, *GetCurrentRoomRequest) (*GetCurrentRoomResponse, error)
GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error)
ListRoomBackgrounds(context.Context, *ListRoomBackgroundsRequest) (*ListRoomBackgroundsResponse, error)
GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error) GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error)
ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error)
ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error)
mustEmbedUnimplementedRoomQueryServiceServer() mustEmbedUnimplementedRoomQueryServiceServer()
} }
@ -1569,12 +1671,18 @@ func (UnimplementedRoomQueryServiceServer) GetCurrentRoom(context.Context, *GetC
func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) { func (UnimplementedRoomQueryServiceServer) GetRoomSnapshot(context.Context, *GetRoomSnapshotRequest) (*GetRoomSnapshotResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRoomSnapshot not implemented") 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) { func (UnimplementedRoomQueryServiceServer) GetRoomTreasure(context.Context, *GetRoomTreasureRequest) (*GetRoomTreasureResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetRoomTreasure not implemented") return nil, status.Error(codes.Unimplemented, "method GetRoomTreasure not implemented")
} }
func (UnimplementedRoomQueryServiceServer) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) { func (UnimplementedRoomQueryServiceServer) ListRoomOnlineUsers(context.Context, *ListRoomOnlineUsersRequest) (*ListRoomOnlineUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomOnlineUsers not implemented") return nil, status.Error(codes.Unimplemented, "method ListRoomOnlineUsers not implemented")
} }
func (UnimplementedRoomQueryServiceServer) ListRoomBannedUsers(context.Context, *ListRoomBannedUsersRequest) (*ListRoomBannedUsersResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListRoomBannedUsers not implemented")
}
func (UnimplementedRoomQueryServiceServer) mustEmbedUnimplementedRoomQueryServiceServer() {} func (UnimplementedRoomQueryServiceServer) mustEmbedUnimplementedRoomQueryServiceServer() {}
func (UnimplementedRoomQueryServiceServer) testEmbeddedByValue() {} func (UnimplementedRoomQueryServiceServer) testEmbeddedByValue() {}
@ -1794,6 +1902,24 @@ func _RoomQueryService_GetRoomSnapshot_Handler(srv interface{}, ctx context.Cont
return interceptor(ctx, in, info, handler) 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) { func _RoomQueryService_GetRoomTreasure_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetRoomTreasureRequest) in := new(GetRoomTreasureRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@ -1830,6 +1956,24 @@ func _RoomQueryService_ListRoomOnlineUsers_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _RoomQueryService_ListRoomBannedUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListRoomBannedUsersRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomQueryServiceServer).ListRoomBannedUsers(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomQueryService_ListRoomBannedUsers_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomQueryServiceServer).ListRoomBannedUsers(ctx, req.(*ListRoomBannedUsersRequest))
}
return interceptor(ctx, in, info, handler)
}
// RoomQueryService_ServiceDesc is the grpc.ServiceDesc for RoomQueryService service. // RoomQueryService_ServiceDesc is the grpc.ServiceDesc for RoomQueryService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@ -1881,6 +2025,10 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetRoomSnapshot", MethodName: "GetRoomSnapshot",
Handler: _RoomQueryService_GetRoomSnapshot_Handler, Handler: _RoomQueryService_GetRoomSnapshot_Handler,
}, },
{
MethodName: "ListRoomBackgrounds",
Handler: _RoomQueryService_ListRoomBackgrounds_Handler,
},
{ {
MethodName: "GetRoomTreasure", MethodName: "GetRoomTreasure",
Handler: _RoomQueryService_GetRoomTreasure_Handler, Handler: _RoomQueryService_GetRoomTreasure_Handler,
@ -1889,6 +2037,10 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListRoomOnlineUsers", MethodName: "ListRoomOnlineUsers",
Handler: _RoomQueryService_ListRoomOnlineUsers_Handler, Handler: _RoomQueryService_ListRoomOnlineUsers_Handler,
}, },
{
MethodName: "ListRoomBannedUsers",
Handler: _RoomQueryService_ListRoomBannedUsers_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "proto/room/v1/room.proto", Metadata: "proto/room/v1/room.proto",

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

@ -6,6 +6,8 @@
`POST /api/v1/rooms/user/kick` `POST /api/v1/rooms/user/kick`
`GET /api/v1/rooms/{room_id}/banned-users`
`POST /api/v1/rooms/user/unban` `POST /api/v1/rooms/user/unban`
本地开发地址: 本地开发地址:
@ -19,6 +21,7 @@ http://127.0.0.1:13000
| 方法 | 接口 | 说明 | | 方法 | 接口 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| `POST` | `/api/v1/rooms/user/kick` | 踢出房间用户,并写入房间 ban 集合。 | | `POST` | `/api/v1/rooms/user/kick` | 踢出房间用户,并写入房间 ban 集合。 |
| `GET` | `/api/v1/rooms/{room_id}/banned-users` | 查询房间维度当前仍有效的踢人列表,也就是房间黑名单列表。 |
| `POST` | `/api/v1/rooms/user/unban` | 解除房间 ban允许用户后续重新 JoinRoom。 | | `POST` | `/api/v1/rooms/user/unban` | 解除房间 ban允许用户后续重新 JoinRoom。 |
## 请求头 ## 请求头
@ -39,6 +42,7 @@ http://127.0.0.1:13000
| `room_id` | 是 | string | `room_10001` | 房间 ID。 | | `room_id` | 是 | string | `room_10001` | 房间 ID。 |
| `command_id` | 是 | string | `cmd_kick_room_10001_20001_1710000000` | 房间命令幂等键;同一次踢人重试必须复用。 | | `command_id` | 是 | string | `cmd_kick_room_10001_20001_1710000000` | 房间命令幂等键;同一次踢人重试必须复用。 |
| `target_user_id` | 是 | int64 | `20001` | 被踢用户长 ID。 | | `target_user_id` | 是 | int64 | `20001` | 被踢用户长 ID。 |
| `duration_ms` | 否 | int64 | `600000` | 禁止重新进房的毫秒时长;`0` 或不传表示永久,直到房间 owner 调用解封。 |
请求示例: 请求示例:
@ -51,7 +55,8 @@ Content-Type: application/json
{ {
"room_id": "room_10001", "room_id": "room_10001",
"command_id": "cmd_kick_room_10001_20001_1710000000", "command_id": "cmd_kick_room_10001_20001_1710000000",
"target_user_id": 20001 "target_user_id": 20001,
"duration_ms": 600000
} }
``` ```
@ -72,6 +77,14 @@ Content-Type: application/json
"room_id": "room_10001", "room_id": "room_10001",
"status": "active", "status": "active",
"ban_user_ids": [20001], "ban_user_ids": [20001],
"ban_states": [
{
"user_id": 20001,
"actor_user_id": 10001,
"created_at_ms": 1710000000456,
"expires_at_ms": 1710000600456
}
],
"online_users": [], "online_users": [],
"mic_seats": [], "mic_seats": [],
"version": 18 "version": 18
@ -86,17 +99,106 @@ Content-Type: application/json
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
| --- | --- | --- | | --- | --- | --- |
| `data.result.applied` | bool | 本次命令是否产生状态变更;重复踢已 ban 且无残留状态的用户可能为 `false`。 | | `data.result.applied` | bool | 本次命令是否产生状态变更;同一个 `command_id` 重试返回同一命令结果,新的 `command_id` 再踢同一用户会刷新 ban 状态。 |
| `data.room.ban_user_ids` | int64[] | 当前房间 ban 用户集合;被踢用户会进入该集合。 | | `data.room.ban_user_ids` | int64[] | 当前房间 ban 用户集合;被踢用户会进入该集合。 |
| `data.room.ban_states` | array | 当前房间 ban 的时间边界;`expires_at_ms=0` 表示永久,非 0 表示该 UTC 毫秒后可重新进房。 |
| `data.room.online_users` | array | 当前业务 presence被踢用户会被移除。 | | `data.room.online_users` | array | 当前业务 presence被踢用户会被移除。 |
| `data.room.mic_seats` | array | 如果被踢用户在麦上,对应麦位会被释放。 | | `data.room.mic_seats` | array | 如果被踢用户在麦上,对应麦位会被释放。 |
| `data.rtc_kicked` | bool | 服务端是否已调用 TRTC `RemoveUserByStrRoomId` 移除目标用户。 | | `data.rtc_kicked` | bool | 服务端是否已调用 TRTC `RemoveUserByStrRoomId` 移除目标用户。 |
| `data.rtc_kick_error` | string | TRTC 服务端踢人失败原因;踢人事实已生效,客户端仍按被踢处理。 | | `data.rtc_kick_error` | string | TRTC 服务端踢人失败原因;踢人事实已生效,客户端仍按被踢处理。 |
## 房间踢人列表
该接口用于房主或房管查看当前房间仍有效的踢人列表。列表只返回当前仍会阻止用户进房的记录;限时踢人已过期后不再返回。被解除的用户也不再返回。
请求示例:
```http
GET /api/v1/rooms/room_10001/banned-users?page=1&page_size=20
Authorization: Bearer <access_token>
X-App-Code: lalu
```
路径参数:
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
| --- | --- | --- | --- | --- |
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
Query 参数:
| 字段 | 必填 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- | --- |
| `page` | 否 | int32 | `1` | 页码,从 1 开始。 |
| `page_size` | 否 | int32 | `20` | 每页数量;建议 App 使用 20后端最大值按实现收敛。 |
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"room_id": "room_10001",
"items": [
{
"user_id": "20001",
"display_user_id": "90020001",
"username": "Alice",
"avatar": "https://example.com/avatar.png",
"country": "US",
"country_name": "United States",
"country_display_name": "United States",
"country_flag": "🇺🇸",
"actor_user_id": "10001",
"created_at_ms": 1710000000456,
"unban_at_ms": 1710000600456,
"remaining_ms": 599000,
"permanent": false
}
],
"total": 1,
"page": 1,
"page_size": 20,
"server_time_ms": 1710000001456
}
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `data.room_id` | string | 当前查询的房间 ID。 |
| `data.items[].user_id` | string | 被踢用户长 IDFlutter 侧按字符串保存,避免大整数精度问题。 |
| `data.items[].display_user_id` | string | 用户展示 ID可能为空端上需要兜底展示 `user_id`。 |
| `data.items[].username` | string | 用户昵称;可能为空,端上可兜底展示 `display_user_id``user_id`。 |
| `data.items[].avatar` | string | 用户头像 URL可能为空端上展示默认头像。 |
| `data.items[].country` | string | 用户国家二位码,例如 `US`。 |
| `data.items[].country_name` | string | 用户国家标准名。 |
| `data.items[].country_display_name` | string | App 展示优先使用的国家名。 |
| `data.items[].country_flag` | string | 国家国旗 emoji为空时端上可按 `country` 本地兜底。 |
| `data.items[].actor_user_id` | string | 执行踢人的用户 ID系统驱逐时可能为 `"0"`。 |
| `data.items[].created_at_ms` | int64 | 踢人状态写入时间UTC epoch milliseconds。 |
| `data.items[].unban_at_ms` | int64 | 自动解封时间;`0` 表示永久,需要手动解除。 |
| `data.items[].remaining_ms` | int64 | 距离自动解除的剩余毫秒;永久踢人为 `0`。 |
| `data.items[].permanent` | bool | 是否永久禁止进房。 |
| `data.total` | int64 | 当前房间有效黑名单总数。 |
| `data.server_time_ms` | int64 | 服务端当前 UTC 毫秒;倒计时 UI 以它校正本地时间。 |
## 解除房间 ban ## 解除房间 ban
解除 ban 只恢复用户后续重新进房资格,不恢复历史 presence、麦位、管理员身份或 RTC 连接。 解除 ban 只恢复用户后续重新进房资格,不恢复历史 presence、麦位、管理员身份或 RTC 连接。
请求体:
| 字段 | 必填 | 类型 | 模拟值 | 说明 |
| --- | --- | --- | --- | --- |
| `room_id` | 是 | string | `room_10001` | 房间 ID。 |
| `command_id` | 是 | string | `cmd_unban_room_10001_20001_1710000100` | 房间命令幂等键;同一次解除操作重试必须复用。 |
| `target_user_id` | 是 | int64 | `20001` | 要解除踢人状态的用户长 ID。 |
请求示例: 请求示例:
```http ```http
@ -112,7 +214,37 @@ Content-Type: application/json
} }
``` ```
成功响应字段和踢人一致,`room.ban_user_ids` 中不再包含目标用户。 成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {
"result": {
"applied": true,
"room_version": 19,
"server_time_ms": 1710000100456
},
"room": {
"room_id": "room_10001",
"status": "active",
"ban_user_ids": [],
"ban_states": [],
"version": 19
}
}
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `data.result.applied` | bool | 是否实际删除了 ban 状态;目标用户本来不在黑名单时为 `false`。 |
| `data.room.ban_user_ids` | int64[] | 当前房间仍有效的 ban 用户集合;成功解除后不再包含目标用户。 |
| `data.room.ban_states` | array | 当前房间仍有效的 ban 状态;成功解除后不再包含目标用户对应状态。 |
## IM 和 RTC 交互 ## IM 和 RTC 交互
@ -211,12 +343,22 @@ C2C 通知和房间群消息使用同一个 `event_id`。Flutter 侧把 `room_us
| --- | --- | | --- | --- |
| 同一个 `command_id` 重试同一次踢人 | 返回同一命令结果,不重复变更房间。 | | 同一个 `command_id` 重试同一次踢人 | 返回同一命令结果,不重复变更房间。 |
| 同一个 `command_id` 换另一个 `target_user_id` | `409 CONFLICT`。 | | 同一个 `command_id` 换另一个 `target_user_id` | `409 CONFLICT`。 |
| 重复踢已 ban 且无残留状态的用户 | `200 OK``applied=false`。 | | 新的 `command_id` 再次踢同一用户 | `200 OK`,刷新 ban 状态和 `duration_ms`。 |
| 解封未 ban 用户 | `200 OK``applied=false`。 | | 解封未 ban 用户 | `200 OK``applied=false`。 |
## Flutter 解析示例 ## Flutter 解析示例
```dart ```dart
int parseIntLike(dynamic value) {
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
return int.tryParse(value?.toString() ?? '') ?? 0;
}
class RoomCommandResult { class RoomCommandResult {
RoomCommandResult({ RoomCommandResult({
required this.applied, required this.applied,
@ -243,12 +385,14 @@ class KickUserResult {
required this.rtcKicked, required this.rtcKicked,
required this.rtcKickError, required this.rtcKickError,
required this.banUserIds, required this.banUserIds,
required this.banStates,
}); });
final RoomCommandResult result; final RoomCommandResult result;
final bool rtcKicked; final bool rtcKicked;
final String rtcKickError; final String rtcKickError;
final List<int> banUserIds; final List<int> banUserIds;
final List<RoomBanState> banStates;
factory KickUserResult.fromJson(Map<String, dynamic> json) { factory KickUserResult.fromJson(Map<String, dynamic> json) {
final room = json['room'] as Map<String, dynamic>? ?? const {}; final room = json['room'] as Map<String, dynamic>? ?? const {};
@ -259,8 +403,119 @@ class KickUserResult {
rtcKicked: json['rtc_kicked'] as bool? ?? false, rtcKicked: json['rtc_kicked'] as bool? ?? false,
rtcKickError: json['rtc_kick_error'] as String? ?? '', rtcKickError: json['rtc_kick_error'] as String? ?? '',
banUserIds: (room['ban_user_ids'] as List<dynamic>? ?? const []) banUserIds: (room['ban_user_ids'] as List<dynamic>? ?? const [])
.map((value) => (value as num).toInt()) .map(parseIntLike)
.toList(), .toList(),
banStates: (room['ban_states'] as List<dynamic>? ?? const [])
.whereType<Map<String, dynamic>>()
.map(RoomBanState.fromJson)
.toList(),
);
}
}
class RoomBanState {
RoomBanState({
required this.userId,
required this.actorUserId,
required this.createdAtMs,
required this.expiresAtMs,
});
final int userId;
final int actorUserId;
final int createdAtMs;
final int expiresAtMs;
bool get permanent => expiresAtMs == 0;
factory RoomBanState.fromJson(Map<String, dynamic> json) {
return RoomBanState(
userId: parseIntLike(json['user_id']),
actorUserId: parseIntLike(json['actor_user_id']),
createdAtMs: parseIntLike(json['created_at_ms']),
expiresAtMs: parseIntLike(json['expires_at_ms']),
);
}
}
class RoomBannedUsersResult {
RoomBannedUsersResult({
required this.roomId,
required this.items,
required this.total,
required this.page,
required this.pageSize,
required this.serverTimeMs,
});
final String roomId;
final List<RoomBannedUserItem> items;
final int total;
final int page;
final int pageSize;
final int serverTimeMs;
factory RoomBannedUsersResult.fromJson(Map<String, dynamic> json) {
return RoomBannedUsersResult(
roomId: json['room_id'] as String? ?? '',
items: (json['items'] as List<dynamic>? ?? const [])
.whereType<Map<String, dynamic>>()
.map(RoomBannedUserItem.fromJson)
.toList(),
total: parseIntLike(json['total']),
page: parseIntLike(json['page']),
pageSize: parseIntLike(json['page_size']),
serverTimeMs: parseIntLike(json['server_time_ms']),
);
}
}
class RoomBannedUserItem {
RoomBannedUserItem({
required this.userId,
required this.displayUserId,
required this.username,
required this.avatar,
required this.country,
required this.countryName,
required this.countryDisplayName,
required this.countryFlag,
required this.actorUserId,
required this.createdAtMs,
required this.unbanAtMs,
required this.remainingMs,
required this.permanent,
});
final int userId;
final String displayUserId;
final String username;
final String avatar;
final String country;
final String countryName;
final String countryDisplayName;
final String countryFlag;
final int actorUserId;
final int createdAtMs;
final int unbanAtMs;
final int remainingMs;
final bool permanent;
factory RoomBannedUserItem.fromJson(Map<String, dynamic> json) {
return RoomBannedUserItem(
userId: parseIntLike(json['user_id']),
displayUserId: json['display_user_id'] as String? ?? '',
username: json['username'] as String? ?? '',
avatar: json['avatar'] as String? ?? '',
country: json['country'] as String? ?? '',
countryName: json['country_name'] as String? ?? '',
countryDisplayName: json['country_display_name'] as String? ?? '',
countryFlag: json['country_flag'] as String? ?? '',
actorUserId: parseIntLike(json['actor_user_id']),
createdAtMs: parseIntLike(json['created_at_ms']),
unbanAtMs: parseIntLike(json['unban_at_ms']),
remainingMs: parseIntLike(json['remaining_ms']),
permanent: json['permanent'] as bool? ?? false,
); );
} }
} }

View File

@ -1069,6 +1069,51 @@ paths:
$ref: "#/responses/Conflict" $ref: "#/responses/Conflict"
"502": "502":
$ref: "#/responses/UpstreamError" $ref: "#/responses/UpstreamError"
/api/v1/rooms/{room_id}/banned-users:
get:
tags:
- rooms
summary: 查询房间黑名单列表
operationId: listRoomBannedUsers
description: 房主或房管分页查看当前仍有效的房间踢人列表;已过期和已解封用户不返回。
security:
- BearerAuth: []
parameters:
- name: room_id
in: path
required: true
type: string
pattern: "^[A-Za-z0-9_-]{1,48}$"
maxLength: 48
- name: page
in: query
required: false
type: integer
format: int32
default: 1
- name: page_size
in: query
required: false
type: integer
format: int32
default: 20
responses:
"200":
description: 返回房间当前仍有效的黑名单用户及展示资料。
schema:
$ref: "#/definitions/RoomBannedUsersEnvelope"
"400":
$ref: "#/responses/BadRequest"
"401":
$ref: "#/responses/Unauthorized"
"403":
$ref: "#/responses/Forbidden"
"404":
$ref: "#/responses/NotFound"
"409":
$ref: "#/responses/Conflict"
"502":
$ref: "#/responses/UpstreamError"
/api/v1/rooms/{room_id}/follow: /api/v1/rooms/{room_id}/follow:
post: post:
tags: tags:
@ -4208,6 +4253,10 @@ definitions:
target_user_id: target_user_id:
type: integer type: integer
format: int64 format: int64
duration_ms:
type: integer
format: int64
description: 0 表示永久禁止进房;大于 0 表示从服务端当前 UTC 时间起禁止进房的毫秒时长。
UnbanUserRequest: UnbanUserRequest:
type: object type: object
properties: properties:
@ -4342,6 +4391,11 @@ definitions:
items: items:
type: integer type: integer
format: int64 format: int64
ban_states:
type: array
description: 房间 ban 的时间边界expires_at_ms 为 0 表示永久。
items:
$ref: "#/definitions/RoomBanState"
mute_user_ids: mute_user_ids:
type: array type: array
items: items:
@ -4362,6 +4416,76 @@ definitions:
version: version:
type: integer type: integer
format: int64 format: int64
RoomBanState:
type: object
properties:
user_id:
type: integer
format: int64
actor_user_id:
type: integer
format: int64
created_at_ms:
type: integer
format: int64
expires_at_ms:
type: integer
format: int64
description: 0 表示永久;非 0 表示该 UTC 毫秒后可重新进房。
RoomBannedUserData:
type: object
properties:
user_id:
type: string
display_user_id:
type: string
username:
type: string
avatar:
type: string
country:
type: string
country_name:
type: string
country_display_name:
type: string
country_flag:
type: string
actor_user_id:
type: string
created_at_ms:
type: integer
format: int64
unban_at_ms:
type: integer
format: int64
description: 0 表示永久,需要手动解除;非 0 表示自动解封 UTC 毫秒。
remaining_ms:
type: integer
format: int64
permanent:
type: boolean
RoomBannedUsersData:
type: object
properties:
room_id:
type: string
items:
type: array
items:
$ref: "#/definitions/RoomBannedUserData"
total:
type: integer
format: int64
page:
type: integer
format: int32
page_size:
type: integer
format: int32
server_time_ms:
type: integer
format: int64
RoomListItemData: RoomListItemData:
type: object type: object
required: required:
@ -5201,6 +5325,13 @@ definitions:
properties: properties:
data: data:
$ref: "#/definitions/UnbanUserResponse" $ref: "#/definitions/UnbanUserResponse"
RoomBannedUsersEnvelope:
allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase"
- type: object
properties:
data:
$ref: "#/definitions/RoomBannedUsersData"
SendGiftEnvelope: SendGiftEnvelope:
allOf: allOf:
- $ref: "#/definitions/GatewayOKEnvelopeBase" - $ref: "#/definitions/GatewayOKEnvelopeBase"

View File

@ -411,6 +411,27 @@ paths:
$ref: "#/definitions/GetRoomSnapshotResponse" $ref: "#/definitions/GetRoomSnapshotResponse"
default: default:
$ref: "#/responses/GRPCError" $ref: "#/responses/GRPCError"
/hyapp.room.v1.RoomQueryService/ListRoomBannedUsers:
post:
tags:
- room-query
summary: 查询房间黑名单列表
operationId: roomListRoomBannedUsers
description: 读取 Room Cell 当前仍有效的 ban 状态viewer 必须是当前房间 owner/admin。
x-grpc-full-method: /hyapp.room.v1.RoomQueryService/ListRoomBannedUsers
parameters:
- name: body
in: body
required: true
schema:
$ref: "#/definitions/ListRoomBannedUsersRequest"
responses:
"200":
description: 返回当前仍有效的房间黑名单。
schema:
$ref: "#/definitions/ListRoomBannedUsersResponse"
default:
$ref: "#/responses/GRPCError"
/grpc.health.v1.Health/Check: /grpc.health.v1.Health/Check:
post: post:
tags: tags:
@ -587,6 +608,11 @@ definitions:
items: items:
type: integer type: integer
format: int64 format: int64
ban_states:
type: array
description: 房间 ban 的时间边界expires_at_ms 为 0 表示永久。
items:
$ref: "#/definitions/RoomBanState"
mute_user_ids: mute_user_ids:
type: array type: array
items: items:
@ -607,6 +633,22 @@ definitions:
version: version:
type: integer type: integer
format: int64 format: int64
RoomBanState:
type: object
properties:
user_id:
type: integer
format: int64
actor_user_id:
type: integer
format: int64
created_at_ms:
type: integer
format: int64
expires_at_ms:
type: integer
format: int64
description: 0 表示永久;非 0 表示该 UTC 毫秒后可重新进房。
CreateRoomRequest: CreateRoomRequest:
type: object type: object
description: 同一个 `meta.actor_user_id` 只能作为 owner 创建一个房间owner/host 不接受外部字段自报。 description: 同一个 `meta.actor_user_id` 只能作为 owner 创建一个房间owner/host 不接受外部字段自报。
@ -882,6 +924,10 @@ definitions:
target_user_id: target_user_id:
type: integer type: integer
format: int64 format: int64
duration_ms:
type: integer
format: int64
description: 0 表示永久禁止进房;大于 0 表示从服务端当前 UTC 时间起禁止进房的毫秒时长。
KickUserResponse: KickUserResponse:
type: object type: object
properties: properties:
@ -1052,6 +1098,63 @@ definitions:
server_time_ms: server_time_ms:
type: integer type: integer
format: int64 format: int64
RoomBannedUser:
type: object
properties:
user_id:
type: integer
format: int64
actor_user_id:
type: integer
format: int64
created_at_ms:
type: integer
format: int64
unban_at_ms:
type: integer
format: int64
description: 0 表示永久;非 0 表示自动解封 UTC 毫秒。
remaining_ms:
type: integer
format: int64
permanent:
type: boolean
ListRoomBannedUsersRequest:
type: object
properties:
meta:
$ref: "#/definitions/RequestMeta"
room_id:
type: string
viewer_user_id:
type: integer
format: int64
description: 由 gateway 从 token user_id 注入,客户端不能自报。
page:
type: integer
format: int32
page_size:
type: integer
format: int32
ListRoomBannedUsersResponse:
type: object
properties:
items:
type: array
items:
$ref: "#/definitions/RoomBannedUser"
total:
type: integer
format: int64
page:
type: integer
format: int32
page_size:
type: integer
format: int32
server_time_ms:
type: integer
format: int64
RoomSystemMessage: RoomSystemMessage:
type: object type: object
properties: properties:

View File

@ -60,10 +60,13 @@
| GET | `/api/v1/rooms/{room_id}/detail` | rooms | `getRoomDetail` | 查询房间详情页首屏聚合包 | | 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}/online-users` | rooms | `listRoomOnlineUsers` | 分页查询房间在线用户 |
| GET | `/api/v1/rooms/{room_id}/gift-panel` | rooms | `getRoomGiftPanel` | 查询房间礼物面板初始化数据 | | 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` | 关注房间 | | POST | `/api/v1/rooms/{room_id}/follow` | rooms | `followRoom` | 关注房间 |
| DELETE | `/api/v1/rooms/{room_id}/follow` | rooms | `unfollowRoom` | 取消关注房间 | | DELETE | `/api/v1/rooms/{room_id}/follow` | rooms | `unfollowRoom` | 取消关注房间 |
| POST | `/api/v1/rooms/create` | rooms | `createRoom` | 创建房间 | | POST | `/api/v1/rooms/create` | rooms | `createRoom` | 创建房间 |
| POST | `/api/v1/rooms/profile/update` | rooms | `updateRoomProfile` | 修改房间资料和座位数 | | 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/join` | rooms | `joinRoom` | 加入房间并返回首屏数据和 RTC token |
| POST | `/api/v1/rooms/heartbeat` | rooms | `roomHeartbeat` | 刷新房间业务 presence | | POST | `/api/v1/rooms/heartbeat` | rooms | `roomHeartbeat` | 刷新房间业务 presence |
| POST | `/api/v1/rooms/leave` | rooms | `leaveRoom` | 离开房间 | | POST | `/api/v1/rooms/leave` | rooms | `leaveRoom` | 离开房间 |

View File

@ -513,6 +513,54 @@ Rules:
- 成功响应返回最新 `room``seats`,客户端直接刷新房间标题、头像、简介和麦位列表。 - 成功响应返回最新 `room``seats`,客户端直接刷新房间标题、头像、简介和麦位列表。
- 成功后房间系统消息事件名为 `room_profile_updated` - 成功后房间系统消息事件名为 `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 ### Heartbeat
```http ```http

View File

@ -11,6 +11,8 @@ import (
type RoomClient interface { type RoomClient interface {
CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest) (*roomv1.CreateRoomResponse, error) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest) (*roomv1.CreateRoomResponse, error)
UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (*roomv1.UpdateRoomProfileResponse, 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) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error)
RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRequest) (*roomv1.RoomHeartbeatResponse, error) RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRequest) (*roomv1.RoomHeartbeatResponse, error)
LeaveRoom(ctx context.Context, req *roomv1.LeaveRoomRequest) (*roomv1.LeaveRoomResponse, error) LeaveRoom(ctx context.Context, req *roomv1.LeaveRoomRequest) (*roomv1.LeaveRoomResponse, error)
@ -47,8 +49,10 @@ type RoomQueryClient interface {
GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error) GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (*roomv1.GetMyRoomResponse, error)
GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error)
GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapshotRequest) (*roomv1.GetRoomSnapshotResponse, 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) GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error)
ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error) ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error)
ListRoomBannedUsers(ctx context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error)
} }
type grpcRoomClient struct { type grpcRoomClient struct {
@ -92,6 +96,14 @@ func (c *grpcRoomClient) UpdateRoomProfile(ctx context.Context, req *roomv1.Upda
return c.client.UpdateRoomProfile(ctx, req) 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) { func (c *grpcRoomClient) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
return c.client.JoinRoom(ctx, req) return c.client.JoinRoom(ctx, req)
} }
@ -200,6 +212,10 @@ func (c *grpcRoomQueryClient) GetRoomSnapshot(ctx context.Context, req *roomv1.G
return c.client.GetRoomSnapshot(ctx, req) 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) { func (c *grpcRoomQueryClient) GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error) {
return c.client.GetRoomTreasure(ctx, req) return c.client.GetRoomTreasure(ctx, req)
} }
@ -207,3 +223,7 @@ func (c *grpcRoomQueryClient) GetRoomTreasure(ctx context.Context, req *roomv1.G
func (c *grpcRoomQueryClient) ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error) { func (c *grpcRoomQueryClient) ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error) {
return c.client.ListRoomOnlineUsers(ctx, req) return c.client.ListRoomOnlineUsers(ctx, req)
} }
func (c *grpcRoomQueryClient) ListRoomBannedUsers(ctx context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
return c.client.ListRoomBannedUsers(ctx, req)
}

View File

@ -120,6 +120,14 @@ func (f *fakeUserLeaderboardRoomQueryClient) GetRoomTreasure(context.Context, *r
return &roomv1.GetRoomTreasureResponse{}, nil return &roomv1.GetRoomTreasureResponse{}, nil
} }
func (f *fakeUserLeaderboardRoomQueryClient) ListRoomBackgrounds(context.Context, *roomv1.ListRoomBackgroundsRequest) (*roomv1.ListRoomBackgroundsResponse, error) {
return &roomv1.ListRoomBackgroundsResponse{}, nil
}
func (f *fakeUserLeaderboardRoomQueryClient) ListRoomOnlineUsers(context.Context, *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error) { func (f *fakeUserLeaderboardRoomQueryClient) ListRoomOnlineUsers(context.Context, *roomv1.ListRoomOnlineUsersRequest) (*roomv1.ListRoomOnlineUsersResponse, error) {
return &roomv1.ListRoomOnlineUsersResponse{}, nil return &roomv1.ListRoomOnlineUsersResponse{}, nil
} }
func (f *fakeUserLeaderboardRoomQueryClient) ListRoomBannedUsers(context.Context, *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
return &roomv1.ListRoomBannedUsersResponse{}, nil
}

View File

@ -100,11 +100,15 @@ type RoomHandlers struct {
GetRoomSnapshot http.HandlerFunc GetRoomSnapshot http.HandlerFunc
GetRoomDetail http.HandlerFunc GetRoomDetail http.HandlerFunc
ListRoomOnlineUsers http.HandlerFunc ListRoomOnlineUsers http.HandlerFunc
ListRoomBannedUsers http.HandlerFunc
GetRoomGiftPanel http.HandlerFunc GetRoomGiftPanel http.HandlerFunc
GetRoomTreasure http.HandlerFunc GetRoomTreasure http.HandlerFunc
FollowRoom http.HandlerFunc FollowRoom http.HandlerFunc
CreateRoom http.HandlerFunc CreateRoom http.HandlerFunc
UpdateRoomProfile http.HandlerFunc UpdateRoomProfile http.HandlerFunc
SaveRoomBackground http.HandlerFunc
ListRoomBackgrounds http.HandlerFunc
SetRoomBackground http.HandlerFunc
JoinRoom http.HandlerFunc JoinRoom http.HandlerFunc
RoomHeartbeat http.HandlerFunc RoomHeartbeat http.HandlerFunc
LeaveRoom http.HandlerFunc LeaveRoom http.HandlerFunc
@ -309,11 +313,15 @@ func (r routes) registerRoomRoutes() {
r.profile("/rooms/snapshot", http.MethodGet, h.GetRoomSnapshot) r.profile("/rooms/snapshot", http.MethodGet, h.GetRoomSnapshot)
r.profile("/rooms/{room_id}/detail", http.MethodGet, h.GetRoomDetail) r.profile("/rooms/{room_id}/detail", http.MethodGet, h.GetRoomDetail)
r.profile("/rooms/{room_id}/online-users", http.MethodGet, h.ListRoomOnlineUsers) r.profile("/rooms/{room_id}/online-users", http.MethodGet, h.ListRoomOnlineUsers)
r.profile("/rooms/{room_id}/banned-users", http.MethodGet, h.ListRoomBannedUsers)
r.profile("/rooms/{room_id}/gift-panel", http.MethodGet, h.GetRoomGiftPanel) 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}/treasure", http.MethodGet, h.GetRoomTreasure)
r.profile("/rooms/{room_id}/follow", "", h.FollowRoom) 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/create", http.MethodPost, h.CreateRoom)
r.profile("/rooms/profile/update", http.MethodPost, h.UpdateRoomProfile) 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/join", http.MethodPost, h.JoinRoom)
r.profile("/rooms/heartbeat", http.MethodPost, h.RoomHeartbeat) r.profile("/rooms/heartbeat", http.MethodPost, h.RoomHeartbeat)
r.profile("/rooms/leave", http.MethodPost, h.LeaveRoom) r.profile("/rooms/leave", http.MethodPost, h.LeaveRoom)

View File

@ -33,32 +33,34 @@ import (
// fakeRoomClient 只承接 gateway transport 测试需要观察的 gRPC 入参。 // fakeRoomClient 只承接 gateway transport 测试需要观察的 gRPC 入参。
// 测试目标是 HTTP envelope 和 RequestMeta 传递,不验证 room-service 业务行为。 // 测试目标是 HTTP envelope 和 RequestMeta 传递,不验证 room-service 业务行为。
type fakeRoomClient struct { type fakeRoomClient struct {
lastCreate *roomv1.CreateRoomRequest lastCreate *roomv1.CreateRoomRequest
lastUpdateProfile *roomv1.UpdateRoomProfileRequest lastUpdateProfile *roomv1.UpdateRoomProfileRequest
lastJoin *roomv1.JoinRoomRequest lastSaveBackground *roomv1.SaveRoomBackgroundRequest
lastHeartbeat *roomv1.RoomHeartbeatRequest lastSetBackground *roomv1.SetRoomBackgroundRequest
lastLeave *roomv1.LeaveRoomRequest lastJoin *roomv1.JoinRoomRequest
lastMicUp *roomv1.MicUpRequest lastHeartbeat *roomv1.RoomHeartbeatRequest
lastMicDown *roomv1.MicDownRequest lastLeave *roomv1.LeaveRoomRequest
lastChangeMicSeat *roomv1.ChangeMicSeatRequest lastMicUp *roomv1.MicUpRequest
lastConfirmMic *roomv1.ConfirmMicPublishingRequest lastMicDown *roomv1.MicDownRequest
lastSetMicMute *roomv1.SetMicMuteRequest lastChangeMicSeat *roomv1.ChangeMicSeatRequest
lastRTCEvent *roomv1.ApplyRTCEventRequest lastConfirmMic *roomv1.ConfirmMicPublishingRequest
lastMicSeatLock *roomv1.SetMicSeatLockRequest lastSetMicMute *roomv1.SetMicMuteRequest
lastChatEnabled *roomv1.SetChatEnabledRequest lastRTCEvent *roomv1.ApplyRTCEventRequest
lastRoomPassword *roomv1.SetRoomPasswordRequest lastMicSeatLock *roomv1.SetMicSeatLockRequest
lastSetAdmin *roomv1.SetRoomAdminRequest lastChatEnabled *roomv1.SetChatEnabledRequest
lastMute *roomv1.MuteUserRequest lastRoomPassword *roomv1.SetRoomPasswordRequest
lastKick *roomv1.KickUserRequest lastSetAdmin *roomv1.SetRoomAdminRequest
lastUnban *roomv1.UnbanUserRequest lastMute *roomv1.MuteUserRequest
lastGift *roomv1.SendGiftRequest lastKick *roomv1.KickUserRequest
sendGiftResp *roomv1.SendGiftResponse lastUnban *roomv1.UnbanUserRequest
lastFollowRoom *roomv1.FollowRoomRequest lastGift *roomv1.SendGiftRequest
lastUnfollowRoom *roomv1.UnfollowRoomRequest sendGiftResp *roomv1.SendGiftResponse
createErr error lastFollowRoom *roomv1.FollowRoomRequest
updateProfileErr error lastUnfollowRoom *roomv1.UnfollowRoomRequest
joinErr error createErr error
joinResp *roomv1.JoinRoomResponse updateProfileErr error
joinErr error
joinResp *roomv1.JoinRoomResponse
} }
func (f *fakeRoomClient) CreateRoom(_ context.Context, req *roomv1.CreateRoomRequest) (*roomv1.CreateRoomResponse, error) { 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 }, 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) { func (f *fakeRoomClient) JoinRoom(_ context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
f.lastJoin = req f.lastJoin = req
if f.joinErr != nil { if f.joinErr != nil {
@ -293,27 +336,33 @@ type fakeUserIdentityClient struct {
} }
type fakeRoomQueryClient struct { type fakeRoomQueryClient struct {
lastList *roomv1.ListRoomsRequest lastList *roomv1.ListRoomsRequest
lastFeeds *roomv1.ListRoomFeedsRequest lastFeeds *roomv1.ListRoomFeedsRequest
lastMyRoom *roomv1.GetMyRoomRequest lastMyRoom *roomv1.GetMyRoomRequest
lastCurrent *roomv1.GetCurrentRoomRequest lastCurrent *roomv1.GetCurrentRoomRequest
lastSnapshot *roomv1.GetRoomSnapshotRequest lastSnapshot *roomv1.GetRoomSnapshotRequest
lastTreasure *roomv1.GetRoomTreasureRequest lastBackgrounds *roomv1.ListRoomBackgroundsRequest
lastOnline *roomv1.ListRoomOnlineUsersRequest lastTreasure *roomv1.GetRoomTreasureRequest
resp *roomv1.ListRoomsResponse lastOnline *roomv1.ListRoomOnlineUsersRequest
feedsResp *roomv1.ListRoomsResponse lastBannedUsers *roomv1.ListRoomBannedUsersRequest
myRoomResp *roomv1.GetMyRoomResponse resp *roomv1.ListRoomsResponse
currentResp *roomv1.GetCurrentRoomResponse feedsResp *roomv1.ListRoomsResponse
snapshotResp *roomv1.GetRoomSnapshotResponse myRoomResp *roomv1.GetMyRoomResponse
treasureResp *roomv1.GetRoomTreasureResponse currentResp *roomv1.GetCurrentRoomResponse
onlineResp *roomv1.ListRoomOnlineUsersResponse snapshotResp *roomv1.GetRoomSnapshotResponse
err error backgroundsResp *roomv1.ListRoomBackgroundsResponse
feedsErr error treasureResp *roomv1.GetRoomTreasureResponse
myRoomErr error onlineResp *roomv1.ListRoomOnlineUsersResponse
currentErr error bannedUsersResp *roomv1.ListRoomBannedUsersResponse
snapshotErr error err error
treasureErr error feedsErr error
onlineErr error myRoomErr error
currentErr error
snapshotErr error
backgroundsErr error
treasureErr error
onlineErr error
bannedUsersErr error
} }
type fakeUserDeviceClient struct { type fakeUserDeviceClient struct {
@ -761,6 +810,17 @@ func (f *fakeRoomQueryClient) GetRoomSnapshot(_ context.Context, req *roomv1.Get
return &roomv1.GetRoomSnapshotResponse{}, nil 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) { func (f *fakeRoomQueryClient) GetRoomTreasure(_ context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error) {
f.lastTreasure = req f.lastTreasure = req
if f.treasureErr != nil { if f.treasureErr != nil {
@ -783,6 +843,17 @@ func (f *fakeRoomQueryClient) ListRoomOnlineUsers(_ context.Context, req *roomv1
return &roomv1.ListRoomOnlineUsersResponse{Page: req.GetPage(), PageSize: req.GetPageSize()}, nil return &roomv1.ListRoomOnlineUsersResponse{Page: req.GetPage(), PageSize: req.GetPageSize()}, nil
} }
func (f *fakeRoomQueryClient) ListRoomBannedUsers(_ context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
f.lastBannedUsers = req
if f.bannedUsersErr != nil {
return nil, f.bannedUsersErr
}
if f.bannedUsersResp != nil {
return f.bannedUsersResp, nil
}
return &roomv1.ListRoomBannedUsersResponse{Page: req.GetPage(), PageSize: req.GetPageSize()}, nil
}
func (f *fakeUserDeviceClient) BindPushToken(_ context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error) { func (f *fakeUserDeviceClient) BindPushToken(_ context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error) {
f.lastBind = req f.lastBind = req
if f.bindErr != nil { if f.bindErr != nil {
@ -1562,7 +1633,7 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
{ {
name: "kick", name: "kick",
path: "/api/v1/rooms/user/kick", path: "/api/v1/rooms/user/kick",
body: `{"room_id":"room-1","command_id":"cmd-kick-1","target_user_id":43}`, body: `{"room_id":"room-1","command_id":"cmd-kick-1","target_user_id":43,"duration_ms":600000}`,
commandID: "cmd-kick-1", commandID: "cmd-kick-1",
extractMeta: func(client *fakeRoomClient) *roomv1.RequestMeta { extractMeta: func(client *fakeRoomClient) *roomv1.RequestMeta {
return client.lastKick.GetMeta() return client.lastKick.GetMeta()
@ -1617,6 +1688,9 @@ func TestRoomCommandsPropagateClientCommandID(t *testing.T) {
if meta.GetActorUserId() != 42 { if meta.GetActorUserId() != 42 {
t.Fatalf("actor user mismatch: %+v", meta) t.Fatalf("actor user mismatch: %+v", meta)
} }
if test.name == "kick" && client.lastKick.GetDurationMs() != 600000 {
t.Fatalf("kick duration_ms was not forwarded: %+v", client.lastKick)
}
}) })
} }
} }
@ -2290,6 +2364,57 @@ func TestListRoomOnlineUsersIncludesProfileGenderAndAge(t *testing.T) {
} }
} }
func TestListRoomBannedUsersIncludesProfileCountryAndUnbanTime(t *testing.T) {
queryClient := &fakeRoomQueryClient{bannedUsersResp: &roomv1.ListRoomBannedUsersResponse{
Items: []*roomv1.RoomBannedUser{
{
UserId: 43,
ActorUserId: 42,
CreatedAtMs: 1000,
UnbanAtMs: 7000,
RemainingMs: 6000,
Permanent: false,
},
},
Total: 1,
Page: 1,
PageSize: 20,
ServerTimeMs: 2000,
}}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
handler.SetRoomQueryClient(queryClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/rooms/room-banned/banned-users?page=1&page_size=20", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
request.Header.Set("X-Request-ID", "req-room-banned-users")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
if queryClient.lastBannedUsers == nil || queryClient.lastBannedUsers.GetRoomId() != "room-banned" || queryClient.lastBannedUsers.GetViewerUserId() != 42 || queryClient.lastBannedUsers.GetPageSize() != 20 {
t.Fatalf("banned users request mismatch: %+v", queryClient.lastBannedUsers)
}
var response httpkit.ResponseEnvelope
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode banned users response failed: %v", err)
}
data, ok := response.Data.(map[string]any)
items, itemsOK := data["items"].([]any)
if response.Code != httpkit.CodeOK || !ok || !itemsOK || len(items) != 1 {
t.Fatalf("banned users response mismatch: %+v", response)
}
first, ok := items[0].(map[string]any)
if !ok || first["user_id"] != "43" || first["actor_user_id"] != "42" || first["unban_at_ms"] != float64(7000) || first["remaining_ms"] != float64(6000) {
t.Fatalf("banned user item mismatch: %+v", first)
}
if first["username"] != "user-43" || first["avatar"] != "https://cdn.example/avatar.png" || first["country"] != "US" || first["country_flag"] != "🇺🇸" {
t.Fatalf("banned user profile fields mismatch: %+v", first)
}
}
func TestGetRoomSnapshotRejectsInvalidRoomIDBeforeGRPC(t *testing.T) { func TestGetRoomSnapshotRejectsInvalidRoomIDBeforeGRPC(t *testing.T) {
queryClient := &fakeRoomQueryClient{} queryClient := &fakeRoomQueryClient{}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{}) handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
@ -4679,6 +4804,74 @@ func TestUpdateRoomProfileRPCErrorWritesEnvelope(t *testing.T) {
assertEnvelope(t, recorder, http.StatusConflict, string(xerr.Conflict), "req-profile-conflict") 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) { func TestCreateRoomRejectsMissingRequiredFieldsBeforeGRPC(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@ -51,11 +51,15 @@ func (h *Handler) RoomHandlers() httproutes.RoomHandlers {
GetRoomSnapshot: h.getRoomSnapshot, GetRoomSnapshot: h.getRoomSnapshot,
GetRoomDetail: h.getRoomDetail, GetRoomDetail: h.getRoomDetail,
ListRoomOnlineUsers: h.listRoomOnlineUsers, ListRoomOnlineUsers: h.listRoomOnlineUsers,
ListRoomBannedUsers: h.listRoomBannedUsers,
GetRoomGiftPanel: h.getRoomGiftPanel, GetRoomGiftPanel: h.getRoomGiftPanel,
GetRoomTreasure: h.getRoomTreasure, GetRoomTreasure: h.getRoomTreasure,
FollowRoom: h.handleRoomFollow, FollowRoom: h.handleRoomFollow,
CreateRoom: h.createRoom, CreateRoom: h.createRoom,
UpdateRoomProfile: h.updateRoomProfile, UpdateRoomProfile: h.updateRoomProfile,
SaveRoomBackground: h.saveRoomBackground,
ListRoomBackgrounds: h.listRoomBackgrounds,
SetRoomBackground: h.setRoomBackground,
JoinRoom: h.joinRoom, JoinRoom: h.joinRoom,
RoomHeartbeat: h.roomHeartbeat, RoomHeartbeat: h.roomHeartbeat,
LeaveRoom: h.leaveRoom, LeaveRoom: h.leaveRoom,

View File

@ -475,6 +475,46 @@ func (h *Handler) listRoomOnlineUsers(writer http.ResponseWriter, request *http.
httpkit.WriteOK(writer, request, roomOnlineUserDataFromProto(resp, h.roomDisplayProfileMap(request, userIDs))) httpkit.WriteOK(writer, request, roomOnlineUserDataFromProto(resp, h.roomDisplayProfileMap(request, userIDs)))
} }
// listRoomBannedUsers 分页返回房间治理面板使用的当前有效黑名单。
func (h *Handler) listRoomBannedUsers(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
}
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
pageSize, ok := httpkit.PositiveInt32Query(request, "page_size", 20)
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.roomQueryClient.ListRoomBannedUsers(request.Context(), &roomv1.ListRoomBannedUsersRequest{
Meta: httpkit.RoomMeta(request, roomID, ""),
RoomId: roomID,
ViewerUserId: auth.UserIDFromContext(request.Context()),
Page: page,
PageSize: pageSize,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
userIDs := make([]int64, 0, len(resp.GetItems()))
for _, item := range resp.GetItems() {
userIDs = append(userIDs, item.GetUserId())
}
httpkit.WriteOK(writer, request, roomBannedUsersDataFromProto(roomID, resp, h.roomDisplayProfileMap(request, userIDs)))
}
// getRoomGiftPanel 返回房间送礼面板初始化数据。 // getRoomGiftPanel 返回房间送礼面板初始化数据。
func (h *Handler) getRoomGiftPanel(writer http.ResponseWriter, request *http.Request) { func (h *Handler) getRoomGiftPanel(writer http.ResponseWriter, request *http.Request) {
if h.roomQueryClient == nil || h.walletClient == nil { if h.roomQueryClient == nil || h.walletClient == nil {
@ -705,6 +745,76 @@ func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Re
httpkit.WriteOK(writer, request, updateRoomProfileDataFromProto(resp)) 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) { func trimOptionalString(value *string) {
if value == nil { if value == nil {
return return
@ -1089,6 +1199,7 @@ func (h *Handler) kickUser(writer http.ResponseWriter, request *http.Request) {
RoomID string `json:"room_id"` RoomID string `json:"room_id"`
CommandID string `json:"command_id"` CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"` TargetUserID int64 `json:"target_user_id"`
DurationMS int64 `json:"duration_ms"`
} }
if !httpkit.Decode(writer, request, &body) { if !httpkit.Decode(writer, request, &body) {
@ -1098,6 +1209,7 @@ func (h *Handler) kickUser(writer http.ResponseWriter, request *http.Request) {
resp, err := h.roomClient.KickUser(request.Context(), &roomv1.KickUserRequest{ resp, err := h.roomClient.KickUser(request.Context(), &roomv1.KickUserRequest{
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID), Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID, TargetUserId: body.TargetUserID,
DurationMs: body.DurationMS,
}) })
httpkit.Write(writer, request, resp, err) httpkit.Write(writer, request, resp, err)
} }

View File

@ -92,6 +92,31 @@ type roomOnlineUserData struct {
Profile *roomDisplayProfileData `json:"profile,omitempty"` Profile *roomDisplayProfileData `json:"profile,omitempty"`
} }
type roomBannedUsersData struct {
RoomID string `json:"room_id"`
Items []roomBannedUserData `json:"items"`
Total int64 `json:"total"`
Page int32 `json:"page"`
PageSize int32 `json:"page_size"`
ServerTimeMS int64 `json:"server_time_ms"`
}
type roomBannedUserData struct {
UserID string `json:"user_id"`
DisplayUserID string `json:"display_user_id"`
Username string `json:"username"`
Avatar string `json:"avatar"`
Country string `json:"country"`
CountryName string `json:"country_name"`
CountryDisplayName string `json:"country_display_name"`
CountryFlag string `json:"country_flag"`
ActorUserID string `json:"actor_user_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UnbanAtMS int64 `json:"unban_at_ms"`
RemainingMS int64 `json:"remaining_ms"`
Permanent bool `json:"permanent"`
}
type setRoomAdminData struct { type setRoomAdminData struct {
Result roomCommandResultData `json:"result"` Result roomCommandResultData `json:"result"`
TargetUserID string `json:"target_user_id"` TargetUserID string `json:"target_user_id"`
@ -220,6 +245,33 @@ type updateRoomProfileData struct {
ServerTimeMS int64 `json:"server_time_ms"` 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 { type roomSnapshotData struct {
Room roomInitialData `json:"room"` Room roomInitialData `json:"room"`
Seats []roomSeatData `json:"seats"` Seats []roomSeatData `json:"seats"`
@ -247,6 +299,7 @@ type roomInitialData struct {
RoomShortID string `json:"room_short_id,omitempty"` RoomShortID string `json:"room_short_id,omitempty"`
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
CoverURL string `json:"cover_url,omitempty"` CoverURL string `json:"cover_url,omitempty"`
BackgroundURL string `json:"background_url,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
OwnerUserID string `json:"owner_user_id,omitempty"` OwnerUserID string `json:"owner_user_id,omitempty"`
Mode string `json:"mode,omitempty"` Mode string `json:"mode,omitempty"`
@ -478,6 +531,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 { func roomSnapshotDataFromProto(resp *roomv1.GetRoomSnapshotResponse) roomSnapshotData {
if resp == nil { if resp == nil {
return roomSnapshotData{} return roomSnapshotData{}
@ -511,6 +616,7 @@ func roomInitialRoomDataFromSnapshot(snapshot *roomv1.RoomSnapshot, roomID strin
RoomShortID: roomShortID, RoomShortID: roomShortID,
Title: ext["title"], Title: ext["title"],
CoverURL: ext["cover_url"], CoverURL: ext["cover_url"],
BackgroundURL: ext["background_url"],
Description: ext["description"], Description: ext["description"],
OwnerUserID: formatOptionalUserID(snapshot.GetOwnerUserId()), OwnerUserID: formatOptionalUserID(snapshot.GetOwnerUserId()),
Mode: snapshot.GetMode(), Mode: snapshot.GetMode(),
@ -702,6 +808,41 @@ func roomOnlineUserDataFromProto(resp *roomv1.ListRoomOnlineUsersResponse, profi
} }
} }
func roomBannedUsersDataFromProto(roomID string, resp *roomv1.ListRoomBannedUsersResponse, profiles map[int64]roomDisplayProfileData) roomBannedUsersData {
if resp == nil {
return roomBannedUsersData{RoomID: roomID}
}
items := make([]roomBannedUserData, 0, len(resp.GetItems()))
for _, user := range resp.GetItems() {
item := roomBannedUserData{
UserID: formatOptionalUserID(user.GetUserId()),
ActorUserID: formatOptionalUserID(user.GetActorUserId()),
CreatedAtMS: user.GetCreatedAtMs(),
UnbanAtMS: user.GetUnbanAtMs(),
RemainingMS: user.GetRemainingMs(),
Permanent: user.GetPermanent(),
}
if profile, ok := profiles[user.GetUserId()]; ok {
item.DisplayUserID = profile.DisplayUserID
item.Username = profile.Username
item.Avatar = profile.Avatar
item.Country = profile.Country
item.CountryName = profile.CountryName
item.CountryDisplayName = profile.CountryDisplayName
item.CountryFlag = profile.CountryFlag
}
items = append(items, item)
}
return roomBannedUsersData{
RoomID: roomID,
Items: items,
Total: resp.GetTotal(),
Page: resp.GetPage(),
PageSize: resp.GetPageSize(),
ServerTimeMS: resp.GetServerTimeMs(),
}
}
func roomOnlineUsersFromLegacyUsers(users []*roomv1.RoomUser) []*roomv1.RoomOnlineUser { func roomOnlineUsersFromLegacyUsers(users []*roomv1.RoomUser) []*roomv1.RoomOnlineUser {
items := make([]*roomv1.RoomOnlineUser, 0, len(users)) items := make([]*roomv1.RoomOnlineUser, 0, len(users))
for _, user := range users { for _, user := range users {

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) 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='房间用户送礼价值统计表'; ) 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(256) 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 ( CREATE TABLE IF NOT EXISTS room_snapshots (
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID', 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.ActorUserID = body.GetActorUserId()
base.Attributes = map[string]string{ base.Attributes = map[string]string{
"room_name": body.GetRoomName(), "room_name": body.GetRoomName(),
"room_avatar": body.GetRoomAvatar(), "room_avatar": body.GetRoomAvatar(),
"room_description": body.GetRoomDescription(), "room_description": body.GetRoomDescription(),
"seat_count": fmt.Sprintf("%d", body.GetSeatCount()), "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 return base, true, nil
case "RoomChatEnabledChanged": case "RoomChatEnabledChanged":
@ -358,6 +371,8 @@ func eventTypeForClient(eventType string) string {
return "room_mic_seat_locked" return "room_mic_seat_locked"
case "RoomProfileUpdated": case "RoomProfileUpdated":
return "room_profile_updated" return "room_profile_updated"
case "RoomBackgroundChanged":
return "room_background_changed"
case "RoomChatEnabledChanged": case "RoomChatEnabledChanged":
return "room_chat_enabled_changed" return "room_chat_enabled_changed"
case "RoomPasswordChanged": 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) { func TestTencentIMPublisherRemovesGroupMemberBeforeKickMessage(t *testing.T) {
paths := make([]string, 0, 2) paths := make([]string, 0, 2)
client, err := tencentim.NewRESTClient(tencentim.RESTConfig{ client, err := tencentim.NewRESTClient(tencentim.RESTConfig{

View File

@ -93,6 +93,17 @@ type UpdateRoomProfile struct {
// Type 返回命令类型。 // Type 返回命令类型。
func (UpdateRoomProfile) Type() string { return "update_room_profile" } 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 定义业务进房请求。 // JoinRoom 定义业务进房请求。
type JoinRoom struct { type JoinRoom struct {
Base Base
@ -309,6 +320,8 @@ type KickUser struct {
Base Base
// TargetUserID 是被踢出并加入 ban 集合的用户。 // TargetUserID 是被踢出并加入 ban 集合的用户。
TargetUserID int64 `json:"target_user_id"` TargetUserID int64 `json:"target_user_id"`
// DurationMS 为 0 表示永久 ban大于 0 表示从命令提交时间起禁止进房的时长。
DurationMS int64 `json:"duration_ms"`
} }
// Type 返回命令类型。 // Type 返回命令类型。
@ -502,6 +515,8 @@ func Deserialize(commandType string, payload []byte) (Command, error) {
cmd = &CreateRoom{} cmd = &CreateRoom{}
case UpdateRoomProfile{}.Type(): case UpdateRoomProfile{}.Type():
cmd = &UpdateRoomProfile{} cmd = &UpdateRoomProfile{}
case SetRoomBackground{}.Type():
cmd = &SetRoomBackground{}
case JoinRoom{}.Type(): case JoinRoom{}.Type():
cmd = &JoinRoom{} cmd = &JoinRoom{}
case RoomHeartbeat{}.Type(): 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

@ -0,0 +1,119 @@
package service
import (
"context"
"sort"
roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/roomid"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/room/state"
)
const (
// defaultRoomBannedUsersPageSize 是房间黑名单面板默认页大小。
defaultRoomBannedUsersPageSize = 20
// maxRoomBannedUsersPageSize 限制单次查询量,避免管理面板一次拉完整治理状态。
maxRoomBannedUsersPageSize = 100
)
// ListRoomBannedUsers 分页读取房间当前仍有效的黑名单。
// 黑名单状态属于 Room Cell用户头像、昵称、国家等展示资料由 gateway 批量查询 user-service。
func (s *Service) ListRoomBannedUsers(ctx context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
roomID := req.GetRoomId()
viewerUserID := req.GetViewerUserId()
if !roomid.ValidStringID(roomID) {
return nil, xerr.New(xerr.InvalidArgument, "room_id is invalid")
}
if viewerUserID <= 0 {
return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required")
}
snapshot, err := s.currentSnapshot(ctx, roomID)
if err != nil {
return nil, err
}
if snapshot == nil || snapshot.GetRoomId() == "" {
return nil, xerr.New(xerr.NotFound, "room not found")
}
if snapshot.GetStatus() != state.RoomStatusActive {
return nil, xerr.New(xerr.RoomClosed, "room closed")
}
current := state.FromProto(snapshot)
if err := requireManagerPresent(current, viewerUserID); err != nil {
// 黑名单是房间治理数据,只允许当前仍在房间内的 owner/admin 查看。
return nil, err
}
nowMS := s.clock.Now().UnixMilli()
items := activeBannedUsersFromState(current, nowMS)
page, pageSize := normalizeRoomBannedUsersPage(req.GetPage(), req.GetPageSize())
total := int64(len(items))
start := (page - 1) * pageSize
if start >= len(items) {
items = nil
} else {
end := start + pageSize
if end > len(items) {
end = len(items)
}
items = items[start:end]
}
return &roomv1.ListRoomBannedUsersResponse{
Items: items,
Total: total,
Page: int32(page),
PageSize: int32(pageSize),
ServerTimeMs: nowMS,
}, nil
}
func activeBannedUsersFromState(current *state.RoomState, nowMS int64) []*roomv1.RoomBannedUser {
if current == nil || len(current.BanUsers) == 0 {
return nil
}
items := make([]*roomv1.RoomBannedUser, 0, len(current.BanUsers))
for _, ban := range current.BanUsers {
if !ban.ActiveAt(nowMS) {
continue
}
remainingMS := int64(0)
if ban.ExpiresAtMS > 0 {
remainingMS = ban.ExpiresAtMS - nowMS
}
items = append(items, &roomv1.RoomBannedUser{
UserId: ban.UserID,
ActorUserId: ban.ActorUserID,
CreatedAtMs: ban.CreatedAtMS,
UnbanAtMs: ban.ExpiresAtMS,
RemainingMs: remainingMS,
Permanent: ban.ExpiresAtMS == 0,
})
}
sort.Slice(items, func(i, j int) bool {
if items[i].GetCreatedAtMs() != items[j].GetCreatedAtMs() {
return items[i].GetCreatedAtMs() > items[j].GetCreatedAtMs()
}
return items[i].GetUserId() < items[j].GetUserId()
})
return items
}
func normalizeRoomBannedUsersPage(rawPage int32, rawPageSize int32) (int, int) {
page := int(rawPage)
if page <= 0 {
page = 1
}
pageSize := int(rawPageSize)
if pageSize <= 0 {
pageSize = defaultRoomBannedUsersPageSize
}
if pageSize > maxRoomBannedUsersPageSize {
pageSize = maxRoomBannedUsersPageSize
}
return page, pageSize
}

View File

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

View File

@ -41,7 +41,7 @@ func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoom
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !currentRoomSnapshotRestorable(snapshot, userID) { if !currentRoomSnapshotRestorable(snapshot, userID, serverTimeMS) {
// 查询语义必须保持只读;读模型滞后由后续命令投影或专项补偿修复。 // 查询语义必须保持只读;读模型滞后由后续命令投影或专项补偿修复。
return emptyCurrentRoomResponse(serverTimeMS), nil return emptyCurrentRoomResponse(serverTimeMS), nil
} }
@ -81,12 +81,12 @@ func emptyCurrentRoomResponse(serverTimeMS int64) *roomv1.GetCurrentRoomResponse
} }
// currentRoomSnapshotRestorable 判定读模型指向的房间是否仍可被客户端恢复。 // currentRoomSnapshotRestorable 判定读模型指向的房间是否仍可被客户端恢复。
func currentRoomSnapshotRestorable(snapshot *roomv1.RoomSnapshot, userID int64) bool { func currentRoomSnapshotRestorable(snapshot *roomv1.RoomSnapshot, userID int64, nowMS int64) bool {
if snapshot == nil || snapshot.GetRoomId() == "" || snapshot.GetStatus() != state.RoomStatusActive { if snapshot == nil || snapshot.GetRoomId() == "" || snapshot.GetStatus() != state.RoomStatusActive {
// 非 active 房间不能恢复,即使用户级 presence 投影还没被清理。 // 非 active 房间不能恢复,即使用户级 presence 投影还没被清理。
return false return false
} }
if containsUserID(snapshot.GetBanUserIds(), userID) { if snapshotUserBanned(snapshot, userID, nowMS) {
// ban 优先于旧 presence避免被踢用户通过本地恢复入口重新收消息。 // ban 优先于旧 presence避免被踢用户通过本地恢复入口重新收消息。
return false return false
} }

View File

@ -35,8 +35,10 @@ func (s *Service) CheckSpeakPermission(ctx context.Context, req *roomv1.CheckSpe
}, nil }, nil
} }
nowMS := s.clock.Now().UnixMilli()
banned := snapshotUserBanned(snapshot, req.GetUserId(), nowMS)
// 允许发言必须同时满足未 ban、未 mute、房间开启聊天、用户在业务 presence 内。 // 允许发言必须同时满足未 ban、未 mute、房间开启聊天、用户在业务 presence 内。
if !containsID(snapshot.GetBanUserIds(), req.GetUserId()) && !containsID(snapshot.GetMuteUserIds(), req.GetUserId()) && snapshot.GetChatEnabled() && findProtoUser(snapshot, req.GetUserId()) != nil { if !banned && !containsID(snapshot.GetMuteUserIds(), req.GetUserId()) && snapshot.GetChatEnabled() && findProtoUser(snapshot, req.GetUserId()) != nil {
return &roomv1.CheckSpeakPermissionResponse{ return &roomv1.CheckSpeakPermissionResponse{
Allowed: true, Allowed: true,
RoomVersion: snapshot.GetVersion(), RoomVersion: snapshot.GetVersion(),
@ -45,7 +47,7 @@ func (s *Service) CheckSpeakPermission(ctx context.Context, req *roomv1.CheckSpe
reason := "not_in_room" reason := "not_in_room"
switch { switch {
case containsID(snapshot.GetBanUserIds(), req.GetUserId()): case banned:
// ban 优先级最高,被踢用户即使仍在腾讯云 IM 群内也不能发言。 // ban 优先级最高,被踢用户即使仍在腾讯云 IM 群内也不能发言。
reason = "user_banned" reason = "user_banned"
case containsID(snapshot.GetMuteUserIds(), req.GetUserId()): case containsID(snapshot.GetMuteUserIds(), req.GetUserId()):
@ -89,7 +91,7 @@ func (s *Service) VerifyRoomPresence(ctx context.Context, req *roomv1.VerifyRoom
}, nil }, nil
} }
if containsID(snapshot.GetBanUserIds(), req.GetUserId()) { if snapshotUserBanned(snapshot, req.GetUserId(), s.clock.Now().UnixMilli()) {
// ban 用户不能订阅,即使快照里还有旧 presence 也以 ban 为准。 // ban 用户不能订阅,即使快照里还有旧 presence 也以 ban 为准。
return &roomv1.VerifyRoomPresenceResponse{ return &roomv1.VerifyRoomPresenceResponse{
Present: false, Present: false,

View File

@ -154,6 +154,21 @@ func containsID(values []int64, target int64) bool {
return slices.Contains(values, target) return slices.Contains(values, target)
} }
func snapshotUserBanned(snapshot *roomv1.RoomSnapshot, userID int64, nowMS int64) bool {
if snapshot == nil || userID <= 0 {
return false
}
for _, ban := range snapshot.GetBanStates() {
if ban.GetUserId() != userID {
continue
}
// ban_states 是带时间边界的权威状态;过期后即使 ban_user_ids 还没刷新也不能继续拦截。
return ban.GetExpiresAtMs() == 0 || ban.GetExpiresAtMs() > nowMS
}
// 旧快照没有 ban_states 时只能按永久 ban 解释 ban_user_ids避免恢复路径放宽权限。
return containsID(snapshot.GetBanUserIds(), userID)
}
func cloneProtoRank(items []state.RankItem) []*roomv1.RankItem { func cloneProtoRank(items []state.RankItem) []*roomv1.RankItem {
// SendGift 响应需要 protobuf 榜单项,不能直接暴露内部 state.RankItem。 // SendGift 响应需要 protobuf 榜单项,不能直接暴露内部 state.RankItem。
result := make([]*roomv1.RankItem, 0, len(items)) result := make([]*roomv1.RankItem, 0, len(items))

View File

@ -10,6 +10,7 @@ import (
roomeventsv1 "hyapp.local/api/proto/events/room/v1" roomeventsv1 "hyapp.local/api/proto/events/room/v1"
roomv1 "hyapp.local/api/proto/room/v1" roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/appcode" "hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/integration" "hyapp/services/room-service/internal/integration"
roomoutbox "hyapp/services/room-service/internal/room/outbox" roomoutbox "hyapp/services/room-service/internal/room/outbox"
roomservice "hyapp/services/room-service/internal/room/service" roomservice "hyapp/services/room-service/internal/room/service"
@ -100,6 +101,160 @@ func TestKickUserRemovesPresenceBanAndRTCConnection(t *testing.T) {
} }
} }
func TestKickUserDurationExpiresAndAllowsJoin(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
now := &fixedRoomTreasureClock{now: time.Date(2026, 5, 28, 8, 0, 0, 0, time.UTC)}
svc := roomservice.New(roomservice.Config{
NodeID: "node-kick-duration-test",
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 1,
Clock: now,
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
roomID := "room-kick-duration"
ownerID := int64(7301)
viewerID := int64(7401)
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, ownerID),
SeatCount: 10,
Mode: "voice",
VisibleRegionId: 8101,
RoomName: "Kick Duration",
RoomShortId: "kick-duration",
}); err != nil {
t.Fatalf("create duration room fixture failed: %v", err)
}
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, viewerID),
Role: "audience",
}); err != nil {
t.Fatalf("join duration target failed: %v", err)
}
kickResp, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
Meta: roomservice.NewRequestMeta(roomID, ownerID),
TargetUserId: viewerID,
DurationMs: 1000,
})
if err != nil {
t.Fatalf("duration kick failed: %v", err)
}
if len(kickResp.GetRoom().GetBanStates()) != 1 || kickResp.GetRoom().GetBanStates()[0].GetExpiresAtMs() != now.Now().Add(time.Second).UnixMilli() {
t.Fatalf("duration kick must persist ban expiration: %+v", kickResp.GetRoom().GetBanStates())
}
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, viewerID),
Role: "audience",
}); err == nil {
t.Fatalf("target must not join before kick duration expires")
}
presenceResp, err := svc.VerifyRoomPresence(ctx, &roomv1.VerifyRoomPresenceRequest{
AppCode: appcode.Default,
RoomId: roomID,
UserId: viewerID,
})
if err != nil {
t.Fatalf("verify presence before expiration failed: %v", err)
}
if presenceResp.GetPresent() || presenceResp.GetReason() != "user_banned" {
t.Fatalf("active duration ban must reject IM guard: %+v", presenceResp)
}
now.now = now.now.Add(2 * time.Second)
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, viewerID),
Role: "audience",
}); err != nil {
t.Fatalf("target must join after kick duration expires: %v", err)
}
}
func TestListRoomBannedUsersReturnsActiveOnlyForManagers(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
now := &fixedRoomTreasureClock{now: time.Date(2026, 5, 29, 8, 0, 0, 0, time.UTC)}
svc := roomservice.New(roomservice.Config{
NodeID: "node-banned-users-test",
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 1,
Clock: now,
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
roomID := "room-banned-users"
ownerID := int64(7501)
expiredID := int64(7502)
activeID := int64(7503)
normalID := int64(7504)
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, ownerID),
SeatCount: 10,
Mode: "voice",
VisibleRegionId: 8101,
RoomName: "Banned Users",
RoomShortId: "banned-users",
}); err != nil {
t.Fatalf("create banned users room failed: %v", err)
}
for _, userID := range []int64{expiredID, activeID, normalID} {
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
Meta: roomservice.NewRequestMeta(roomID, userID),
Role: "audience",
}); err != nil {
t.Fatalf("join fixture user %d failed: %v", userID, err)
}
}
if _, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
Meta: roomservice.NewRequestMeta(roomID, ownerID),
TargetUserId: expiredID,
DurationMs: 1000,
}); err != nil {
t.Fatalf("kick expired fixture user failed: %v", err)
}
now.now = now.now.Add(2 * time.Second)
activeCreatedAt := now.Now().UnixMilli()
activeUnbanAt := now.Now().Add(10 * time.Second).UnixMilli()
if _, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
Meta: roomservice.NewRequestMeta(roomID, ownerID),
TargetUserId: activeID,
DurationMs: 10_000,
}); err != nil {
t.Fatalf("kick active fixture user failed: %v", err)
}
if _, err := svc.ListRoomBannedUsers(ctx, &roomv1.ListRoomBannedUsersRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
RoomId: roomID,
ViewerUserId: normalID,
Page: 1,
PageSize: 10,
}); !xerr.IsCode(err, xerr.PermissionDenied) {
t.Fatalf("non-manager list banned users must be denied: %v", err)
}
resp, err := svc.ListRoomBannedUsers(ctx, &roomv1.ListRoomBannedUsersRequest{
Meta: &roomv1.RequestMeta{AppCode: appcode.Default, RoomId: roomID},
RoomId: roomID,
ViewerUserId: ownerID,
Page: 1,
PageSize: 10,
})
if err != nil {
t.Fatalf("owner list banned users failed: %v", err)
}
if resp.GetTotal() != 1 || len(resp.GetItems()) != 1 {
t.Fatalf("only active ban must be returned: %+v", resp)
}
item := resp.GetItems()[0]
if item.GetUserId() != activeID || item.GetActorUserId() != ownerID || item.GetCreatedAtMs() != activeCreatedAt || item.GetUnbanAtMs() != activeUnbanAt || item.GetRemainingMs() != 10_000 || item.GetPermanent() {
t.Fatalf("active ban item mismatch: %+v", item)
}
}
func TestCloseRoomByAdminEvictsCurrentUsersThroughOutboxAndRTC(t *testing.T) { func TestCloseRoomByAdminEvictsCurrentUsersThroughOutboxAndRTC(t *testing.T) {
ctx := context.Background() ctx := context.Background()
repository := mysqltest.NewRepository(t) repository := mysqltest.NewRepository(t)

View File

@ -94,6 +94,7 @@ func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*r
cmd := command.KickUser{ cmd := command.KickUser{
Base: baseFromMeta(req.GetMeta()), Base: baseFromMeta(req.GetMeta()),
TargetUserID: req.GetTargetUserId(), TargetUserID: req.GetTargetUserId(),
DurationMS: req.GetDurationMs(),
} }
shouldKickRTC := false shouldKickRTC := false
@ -107,6 +108,9 @@ func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*r
if cmd.TargetUserID == cmd.ActorUserID() { if cmd.TargetUserID == cmd.ActorUserID() {
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "cannot kick self") return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "cannot kick self")
} }
if cmd.DurationMS < 0 {
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "duration_ms must be non-negative")
}
if err := requireActorPresent(current, cmd.ActorUserID()); err != nil { if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
return mutationResult{}, nil, err return mutationResult{}, nil, err
} }
@ -116,10 +120,6 @@ func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*r
_, inRoom := current.OnlineUsers[cmd.TargetUserID] _, inRoom := current.OnlineUsers[cmd.TargetUserID]
_, onSeat := current.SeatByUser(cmd.TargetUserID) _, onSeat := current.SeatByUser(cmd.TargetUserID)
if current.BanUsers[cmd.TargetUserID] && !inRoom && !onSeat && !current.AdminUsers[cmd.TargetUserID] {
// 已处于 ban 且没有残留 presence、麦位或管理员身份时重复 KickUser 不产生新事件。
return mutationResult{snapshot: current.ToProto()}, nil, nil
}
// 只有目标确实还在业务房间或麦位上,才需要请求 TRTC 服务端移除实时音频连接。 // 只有目标确实还在业务房间或麦位上,才需要请求 TRTC 服务端移除实时音频连接。
shouldKickRTC = inRoom || onSeat shouldKickRTC = inRoom || onSeat
@ -136,7 +136,20 @@ func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*r
// ban 集合会让后续 JoinRoom 和 VerifyRoomPresence 都失败。 // ban 集合会让后续 JoinRoom 和 VerifyRoomPresence 都失败。
delete(current.OnlineUsers, cmd.TargetUserID) delete(current.OnlineUsers, cmd.TargetUserID)
delete(current.AdminUsers, cmd.TargetUserID) delete(current.AdminUsers, cmd.TargetUserID)
current.BanUsers[cmd.TargetUserID] = true expiresAtMS := int64(0)
nowMS := now.UnixMilli()
if cmd.DurationMS > 0 {
expiresAtMS = nowMS + cmd.DurationMS
if expiresAtMS <= nowMS {
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "duration_ms is too large")
}
}
current.BanUsers[cmd.TargetUserID] = state.UserModerationState{
UserID: cmd.TargetUserID,
ActorUserID: cmd.ActorUserID(),
CreatedAtMS: nowMS,
ExpiresAtMS: expiresAtMS,
}
current.Version++ current.Version++
kickEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version, now, &roomeventsv1.RoomUserKicked{ kickEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version, now, &roomeventsv1.RoomUserKicked{
@ -207,7 +220,7 @@ func (s *Service) UnbanUser(ctx context.Context, req *roomv1.UnbanUserRequest) (
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil { if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
return mutationResult{}, nil, err return mutationResult{}, nil, err
} }
if !current.BanUsers[cmd.TargetUserID] { if _, exists := current.BanUsers[cmd.TargetUserID]; !exists {
// 重复 unban 是 no-op仍写 command log 确保 command_id 幂等可判定。 // 重复 unban 是 no-op仍写 command log 确保 command_id 幂等可判定。
return mutationResult{snapshot: current.ToProto()}, nil, nil return mutationResult{snapshot: current.ToProto()}, nil, nil
} }

View File

@ -38,9 +38,13 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
} }
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) { result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
if current.BanUsers[cmd.ActorUserID()] { if ban, exists := current.BanUsers[cmd.ActorUserID()]; exists {
// 被踢或封禁用户不能重新进入业务 presence。 if ban.ActiveAt(now.UnixMilli()) {
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned") // 被踢或封禁用户在 ban 未过期前不能重新进入业务 presence。
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned")
}
// 限时 ban 过期后由下一次进房命令清理,避免定时任务介入 Room Cell owner 状态。
delete(current.BanUsers, cmd.ActorUserID())
} }
if existing, exists := current.OnlineUsers[cmd.ActorUserID()]; exists { if existing, exists := current.OnlineUsers[cmd.ActorUserID()]; exists {
@ -138,9 +142,13 @@ func (s *Service) RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRe
if err := requireActiveRoom(current); err != nil { if err := requireActiveRoom(current); err != nil {
return mutationResult{}, nil, err return mutationResult{}, nil, err
} }
if current.BanUsers[cmd.ActorUserID()] { if ban, exists := current.BanUsers[cmd.ActorUserID()]; exists {
// ban 用户不能通过心跳续住业务 presenceKickUser 已经清理 presence这里是防御脏状态。 if ban.ActiveAt(now.UnixMilli()) {
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned") // ban 用户不能通过心跳续住业务 presenceKickUser 已经清理 presence这里是防御脏状态。
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is banned")
}
// 过期 ban 不再阻断心跳;保守清理后继续按 presence 是否存在判断。
delete(current.BanUsers, cmd.ActorUserID())
} }
existing, exists := current.OnlineUsers[cmd.ActorUserID()] existing, exists := current.OnlineUsers[cmd.ActorUserID()]

View File

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

View File

@ -108,7 +108,7 @@ func (s *Service) recoverRoom(ctx context.Context, roomMeta RoomMeta) (*state.Ro
return nil, err return nil, err
} }
if err := replay(recovered, cmd); err != nil { if err := replay(recovered, cmd, record.CreatedAtMS); err != nil {
// replay 失败说明命令日志无法被当前状态机解释,不能继续装载错误 Cell。 // replay 失败说明命令日志无法被当前状态机解释,不能继续装载错误 Cell。
return nil, err return nil, err
} }
@ -118,7 +118,7 @@ func (s *Service) recoverRoom(ctx context.Context, roomMeta RoomMeta) (*state.Ro
} }
// replay 只重放房间内确定性状态变更,不重复执行钱包扣费或腾讯云 IM 投递。 // replay 只重放房间内确定性状态变更,不重复执行钱包扣费或腾讯云 IM 投递。
func replay(current *state.RoomState, cmd command.Command) error { func replay(current *state.RoomState, cmd command.Command, committedAtMS int64) error {
// replay 只重建房间内状态,不重复写 outbox、不重复调用 wallet、不重复同步腾讯云 IM。 // replay 只重建房间内状态,不重复写 outbox、不重复调用 wallet、不重复同步腾讯云 IM。
switch typed := cmd.(type) { switch typed := cmd.(type) {
case *command.CreateRoom: case *command.CreateRoom:
@ -139,6 +139,12 @@ func replay(current *state.RoomState, cmd command.Command) error {
return err return err
} }
current.Version++ 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: case *command.JoinRoom:
if existing, exists := current.OnlineUsers[typed.ActorUserID()]; exists { if existing, exists := current.OnlineUsers[typed.ActorUserID()]; exists {
// 重复 JoinRoom 回放只刷新 last_seen不能重置首次进房时间或降级 owner 角色。 // 重复 JoinRoom 回放只刷新 last_seen不能重置首次进房时间或降级 owner 角色。
@ -334,7 +340,20 @@ func replay(current *state.RoomState, cmd command.Command) error {
} }
delete(current.OnlineUsers, typed.TargetUserID) delete(current.OnlineUsers, typed.TargetUserID)
delete(current.AdminUsers, typed.TargetUserID) delete(current.AdminUsers, typed.TargetUserID)
current.BanUsers[typed.TargetUserID] = true createdAtMS := committedAtMS
if createdAtMS <= 0 {
createdAtMS = typed.SentAtMS
}
expiresAtMS := int64(0)
if typed.DurationMS > 0 && createdAtMS > 0 {
expiresAtMS = createdAtMS + typed.DurationMS
}
current.BanUsers[typed.TargetUserID] = state.UserModerationState{
UserID: typed.TargetUserID,
ActorUserID: typed.ActorUserID(),
CreatedAtMS: createdAtMS,
ExpiresAtMS: expiresAtMS,
}
current.Version++ current.Version++
case *command.UnbanUser: case *command.UnbanUser:
// Unban 只解除 ban不恢复被 KickUser 删除的 presence、麦位或管理员身份。 // Unban 只解除 ban不恢复被 KickUser 删除的 presence、麦位或管理员身份。

View File

@ -92,6 +92,18 @@ type RoomCloseInfo struct {
ClearOnOpen bool 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 中的一条最新快照。 // SnapshotRecord 对应 room_snapshots 中的一条最新快照。
type SnapshotRecord struct { type SnapshotRecord struct {
// AppCode 是快照所属 App恢复时必须和房间 meta 保持一致。 // AppCode 是快照所属 App恢复时必须和房间 meta 保持一致。
@ -480,6 +492,12 @@ type Repository interface {
GetRoomMeta(ctx context.Context, roomID string) (RoomMeta, bool, error) GetRoomMeta(ctx context.Context, roomID string) (RoomMeta, bool, error)
// GetRoomMetaByOwner 查询 owner 已创建的房间;产品规则要求一个用户只能创建一个房间。 // GetRoomMetaByOwner 查询 owner 已创建的房间;产品规则要求一个用户只能创建一个房间。
GetRoomMetaByOwner(ctx context.Context, ownerUserID int64) (RoomMeta, bool, error) 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 读取已提交命令,用于幂等重试和 command_id 冲突判定。
GetCommand(ctx context.Context, roomID string, commandID string) (CommandRecord, bool, error) GetCommand(ctx context.Context, roomID string, commandID string) (CommandRecord, bool, error)
// SaveCommand 追加成功命令日志,关键命令恢复必须依赖它。 // SaveCommand 追加成功命令日志,关键命令恢复必须依赖它。

View File

@ -71,7 +71,7 @@ func (s *Service) GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasu
if snapshot.GetStatus() != state.RoomStatusActive { if snapshot.GetStatus() != state.RoomStatusActive {
return nil, xerr.New(xerr.RoomClosed, "room closed") return nil, xerr.New(xerr.RoomClosed, "room closed")
} }
if containsUserID(snapshot.GetBanUserIds(), viewerUserID) || findProtoUser(snapshot, viewerUserID) == nil { if snapshotUserBanned(snapshot, viewerUserID, s.clock.Now().UnixMilli()) || findProtoUser(snapshot, viewerUserID) == nil {
// 宝箱状态属于房间内活动信息,只允许仍在业务 presence 内的用户读取。 // 宝箱状态属于房间内活动信息,只允许仍在业务 presence 内的用户读取。
return nil, xerr.New(xerr.PermissionDenied, "viewer is not in room") return nil, xerr.New(xerr.PermissionDenied, "viewer is not in room")
} }

View File

@ -43,7 +43,7 @@ func (s *Service) GetRoomSnapshot(ctx context.Context, req *roomv1.GetRoomSnapsh
if snapshot.GetStatus() != state.RoomStatusActive { if snapshot.GetStatus() != state.RoomStatusActive {
return nil, xerr.New(xerr.RoomClosed, "room closed") return nil, xerr.New(xerr.RoomClosed, "room closed")
} }
if containsUserID(snapshot.GetBanUserIds(), viewerUserID) || findProtoUser(snapshot, viewerUserID) == nil { if snapshotUserBanned(snapshot, viewerUserID, s.clock.Now().UnixMilli()) || findProtoUser(snapshot, viewerUserID) == nil {
// 完整快照包含在线用户、麦位和房管集合,必须要求 viewer 仍在业务 presence 内。 // 完整快照包含在线用户、麦位和房管集合,必须要求 viewer 仍在业务 presence 内。
return nil, xerr.New(xerr.PermissionDenied, "viewer is not in room") return nil, xerr.New(xerr.PermissionDenied, "viewer is not in room")
} }

View File

@ -67,7 +67,7 @@ func (s *Service) SystemEvictUser(ctx context.Context, req *roomv1.SystemEvictUs
_, inRoom := current.OnlineUsers[cmd.TargetUserID] _, inRoom := current.OnlineUsers[cmd.TargetUserID]
_, onSeat := current.SeatByUser(cmd.TargetUserID) _, onSeat := current.SeatByUser(cmd.TargetUserID)
alreadyBanned := current.BanUsers[cmd.TargetUserID] _, alreadyBanned := current.BanUsers[cmd.TargetUserID]
if !inRoom && !onSeat && !current.AdminUsers[cmd.TargetUserID] && (!cmd.BanFromRoom || alreadyBanned) { if !inRoom && !onSeat && !current.AdminUsers[cmd.TargetUserID] && (!cmd.BanFromRoom || alreadyBanned) {
// 重复系统驱逐保持幂等;没有残留状态时只返回当前快照。 // 重复系统驱逐保持幂等;没有残留状态时只返回当前快照。
return mutationResult{snapshot: current.ToProto()}, nil, nil return mutationResult{snapshot: current.ToProto()}, nil, nil
@ -89,7 +89,11 @@ func (s *Service) SystemEvictUser(ctx context.Context, req *roomv1.SystemEvictUs
delete(current.AdminUsers, cmd.TargetUserID) delete(current.AdminUsers, cmd.TargetUserID)
if cmd.BanFromRoom { if cmd.BanFromRoom {
// BanFromRoom 由治理入口决定;只驱逐不封房间时,用户后续可按产品策略重新进房。 // BanFromRoom 由治理入口决定;只驱逐不封房间时,用户后续可按产品策略重新进房。
current.BanUsers[cmd.TargetUserID] = true current.BanUsers[cmd.TargetUserID] = state.UserModerationState{
UserID: cmd.TargetUserID,
ActorUserID: 0,
CreatedAtMS: now.UnixMilli(),
}
} }
current.Version++ current.Version++

View File

@ -98,6 +98,23 @@ type RoomUserState struct {
LastSeenAtMS int64 LastSeenAtMS int64
} }
// UserModerationState 保存房间内 ban/mute 这类治理状态的时间边界。
type UserModerationState struct {
// UserID 是被治理用户。
UserID int64
// ActorUserID 是触发治理命令的房间内操作者;系统治理入口使用 0。
ActorUserID int64
// CreatedAtMS 是治理状态写入 Room Cell 的 UTC epoch milliseconds。
CreatedAtMS int64
// ExpiresAtMS 为 0 表示永久有效;非 0 表示该 UTC 毫秒后自动失效。
ExpiresAtMS int64
}
// ActiveAt 判断治理状态在指定 UTC 毫秒是否仍然生效。
func (m UserModerationState) ActiveAt(nowMS int64) bool {
return m.UserID > 0 && (m.ExpiresAtMS == 0 || m.ExpiresAtMS > nowMS)
}
// RankItem 表达房间内本地礼物榜的单个用户累计值。 // RankItem 表达房间内本地礼物榜的单个用户累计值。
type RankItem struct { type RankItem struct {
// UserID 是榜单归属用户。 // UserID 是榜单归属用户。
@ -189,8 +206,8 @@ type RoomState struct {
OnlineUsers map[int64]*RoomUserState OnlineUsers map[int64]*RoomUserState
// AdminUsers 是房间管理权限集合。 // AdminUsers 是房间管理权限集合。
AdminUsers map[int64]bool AdminUsers map[int64]bool
// BanUsers 是被踢或封禁的房间用户集合 // BanUsers 是被踢或封禁的房间用户状态,过期时间由 Room Cell 持久化和恢复
BanUsers map[int64]bool BanUsers map[int64]UserModerationState
// MuteUsers 是被禁言用户集合。 // MuteUsers 是被禁言用户集合。
MuteUsers map[int64]bool MuteUsers map[int64]bool
// Heat 是房间热度,当前主要由送礼累计。 // Heat 是房间热度,当前主要由送礼累计。
@ -223,7 +240,7 @@ func NewRoomState(roomID string, ownerUserID int64, seatCount int32, mode string
MicSeats: seats, MicSeats: seats,
OnlineUsers: make(map[int64]*RoomUserState), OnlineUsers: make(map[int64]*RoomUserState),
AdminUsers: make(map[int64]bool), AdminUsers: make(map[int64]bool),
BanUsers: make(map[int64]bool), BanUsers: make(map[int64]UserModerationState),
MuteUsers: make(map[int64]bool), MuteUsers: make(map[int64]bool),
RoomExt: make(map[string]string), RoomExt: make(map[string]string),
} }
@ -248,7 +265,7 @@ func (s *RoomState) Clone() *RoomState {
MicSeats: append([]MicSeat(nil), s.MicSeats...), MicSeats: append([]MicSeat(nil), s.MicSeats...),
OnlineUsers: make(map[int64]*RoomUserState, len(s.OnlineUsers)), OnlineUsers: make(map[int64]*RoomUserState, len(s.OnlineUsers)),
AdminUsers: make(map[int64]bool, len(s.AdminUsers)), AdminUsers: make(map[int64]bool, len(s.AdminUsers)),
BanUsers: make(map[int64]bool, len(s.BanUsers)), BanUsers: make(map[int64]UserModerationState, len(s.BanUsers)),
MuteUsers: make(map[int64]bool, len(s.MuteUsers)), MuteUsers: make(map[int64]bool, len(s.MuteUsers)),
Heat: s.Heat, Heat: s.Heat,
GiftRank: append([]RankItem(nil), s.GiftRank...), GiftRank: append([]RankItem(nil), s.GiftRank...),
@ -387,7 +404,7 @@ func (s *RoomState) ToProto() *roomv1.RoomSnapshot {
MicSeats: seats, MicSeats: seats,
OnlineUsers: users, OnlineUsers: users,
AdminUserIds: sortedSetIDsExcept(s.AdminUsers, s.OwnerUserID), AdminUserIds: sortedSetIDsExcept(s.AdminUsers, s.OwnerUserID),
BanUserIds: sortedSetIDs(s.BanUsers), BanUserIds: sortedModerationIDs(s.BanUsers),
MuteUserIds: sortedSetIDs(s.MuteUsers), MuteUserIds: sortedSetIDs(s.MuteUsers),
GiftRank: rankItems, GiftRank: rankItems,
RoomExt: cloneStringMap(s.RoomExt), RoomExt: cloneStringMap(s.RoomExt),
@ -396,6 +413,7 @@ func (s *RoomState) ToProto() *roomv1.RoomSnapshot {
RoomShortId: s.RoomExt["room_short_id"], RoomShortId: s.RoomExt["room_short_id"],
Locked: s.RoomPasswordHash != "", Locked: s.RoomPasswordHash != "",
Treasure: treasureStateToProto(s.Treasure), Treasure: treasureStateToProto(s.Treasure),
BanStates: sortedModerationStates(s.BanUsers),
} }
} }
@ -417,7 +435,7 @@ func FromProto(snapshot *roomv1.RoomSnapshot) *RoomState {
MicSeats: make([]MicSeat, 0, len(snapshot.GetMicSeats())), MicSeats: make([]MicSeat, 0, len(snapshot.GetMicSeats())),
OnlineUsers: make(map[int64]*RoomUserState, len(snapshot.GetOnlineUsers())), OnlineUsers: make(map[int64]*RoomUserState, len(snapshot.GetOnlineUsers())),
AdminUsers: make(map[int64]bool, len(snapshot.GetAdminUserIds())), AdminUsers: make(map[int64]bool, len(snapshot.GetAdminUserIds())),
BanUsers: make(map[int64]bool, len(snapshot.GetBanUserIds())), BanUsers: make(map[int64]UserModerationState, len(snapshot.GetBanStates())+len(snapshot.GetBanUserIds())),
MuteUsers: make(map[int64]bool, len(snapshot.GetMuteUserIds())), MuteUsers: make(map[int64]bool, len(snapshot.GetMuteUserIds())),
GiftRank: make([]RankItem, 0, len(snapshot.GetGiftRank())), GiftRank: make([]RankItem, 0, len(snapshot.GetGiftRank())),
RoomExt: cloneStringMap(snapshot.GetRoomExt()), RoomExt: cloneStringMap(snapshot.GetRoomExt()),
@ -460,8 +478,26 @@ func FromProto(snapshot *roomv1.RoomSnapshot) *RoomState {
restored.AdminUsers[userID] = true restored.AdminUsers[userID] = true
} }
for _, ban := range snapshot.GetBanStates() {
if ban.GetUserId() <= 0 {
continue
}
restored.BanUsers[ban.GetUserId()] = UserModerationState{
UserID: ban.GetUserId(),
ActorUserID: ban.GetActorUserId(),
CreatedAtMS: ban.GetCreatedAtMs(),
ExpiresAtMS: ban.GetExpiresAtMs(),
}
}
for _, userID := range snapshot.GetBanUserIds() { for _, userID := range snapshot.GetBanUserIds() {
restored.BanUsers[userID] = true if userID <= 0 {
continue
}
if _, exists := restored.BanUsers[userID]; !exists {
// 旧快照只有 ban_user_ids 时按永久 ban 恢复,避免恢复后放宽权限。
restored.BanUsers[userID] = UserModerationState{UserID: userID}
}
} }
for _, userID := range snapshot.GetMuteUserIds() { for _, userID := range snapshot.GetMuteUserIds() {
@ -495,6 +531,32 @@ func sortedSetIDs(values map[int64]bool) []int64 {
return ids return ids
} }
func sortedModerationIDs(values map[int64]UserModerationState) []int64 {
ids := make([]int64, 0, len(values))
for userID, moderation := range values {
if moderation.UserID > 0 {
ids = append(ids, userID)
}
}
slices.Sort(ids)
return ids
}
func sortedModerationStates(values map[int64]UserModerationState) []*roomv1.RoomBanState {
ids := sortedModerationIDs(values)
states := make([]*roomv1.RoomBanState, 0, len(ids))
for _, userID := range ids {
moderation := values[userID]
states = append(states, &roomv1.RoomBanState{
UserId: moderation.UserID,
ActorUserId: moderation.ActorUserID,
CreatedAtMs: moderation.CreatedAtMS,
ExpiresAtMs: moderation.ExpiresAtMS,
})
}
return states
}
func sortedSetIDsExcept(values map[int64]bool, excluded int64) []int64 { func sortedSetIDsExcept(values map[int64]bool, excluded int64) []int64 {
ids := make([]int64, 0, len(values)) ids := make([]int64, 0, len(values))
for userID, enabled := range values { for userID, enabled := range values {

View File

@ -147,6 +147,18 @@ func (r *Repository) Migrate(ctx context.Context) error {
PRIMARY KEY (app_code, room_id, user_id), 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) 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`, ) 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(256) 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 ( `CREATE TABLE IF NOT EXISTS room_snapshots (
app_code VARCHAR(32) NOT NULL, app_code VARCHAR(32) NOT NULL,
room_id VARCHAR(64) 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 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 冲突判定。 // GetCommand 读取已提交命令,用于幂等重试和 command_id 冲突判定。
func (r *Repository) GetCommand(ctx context.Context, roomID string, commandID string) (roomservice.CommandRecord, bool, error) { func (r *Repository) GetCommand(ctx context.Context, roomID string, commandID string) (roomservice.CommandRecord, bool, error) {
// 幂等检查只看 command log只有成功提交过的命令才算 seen。 // 幂等检查只看 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)) 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 代理到领域服务。 // JoinRoom 代理到领域服务。
func (s *Server) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) { func (s *Server) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
// JoinRoom 只维护 room-service presence真实消息连接由腾讯云 IM SDK 处理。 // 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)) 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 代理到房间宝箱只读查询。 // GetRoomTreasure 代理到房间宝箱只读查询。
func (s *Server) GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error) { func (s *Server) GetRoomTreasure(ctx context.Context, req *roomv1.GetRoomTreasureRequest) (*roomv1.GetRoomTreasureResponse, error) {
// 宝箱查询不刷新 presence当前进度仍以 Room Cell 快照和 UTC 日边界为准。 // 宝箱查询不刷新 presence当前进度仍以 Room Cell 快照和 UTC 日边界为准。
@ -482,6 +507,18 @@ func (s *Server) ListRoomOnlineUsers(ctx context.Context, req *roomv1.ListRoomOn
return mapServiceResult(s.svc.ListRoomOnlineUsers(ctx, req)) return mapServiceResult(s.svc.ListRoomOnlineUsers(ctx, req))
} }
// ListRoomBannedUsers 代理到房间黑名单分页读模型。
func (s *Server) ListRoomBannedUsers(ctx context.Context, req *roomv1.ListRoomBannedUsersRequest) (*roomv1.ListRoomBannedUsersResponse, error) {
// 黑名单来自 Room Cell必须按 room owner 路由读取最新治理状态。
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.ListRoomBannedUsersResponse, error) {
return client.ListRoomBannedUsers(callCtx, req)
}); forwarded {
return resp, err
}
return mapServiceResult(s.svc.ListRoomBannedUsers(ctx, req))
}
func contextWithMetaApp(ctx context.Context, meta *roomv1.RequestMeta) context.Context { func contextWithMetaApp(ctx context.Context, meta *roomv1.RequestMeta) context.Context {
if meta == nil { if meta == nil {
return appcode.WithContext(ctx, "") return appcode.WithContext(ctx, "")