完善房间

This commit is contained in:
zhx 2026-05-11 19:26:01 +08:00
parent 8fcf491b74
commit d1fbeb54a8
52 changed files with 5895 additions and 3718 deletions

View File

@ -210,6 +210,84 @@ func (x *RoomCreated) GetRoomDescription() string {
return ""
}
// RoomProfileUpdated 表达房间展示资料或麦位数量变更。
type RoomProfileUpdated struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ActorUserId int64 `protobuf:"varint,1,opt,name=actor_user_id,json=actorUserId,proto3" json:"actor_user_id,omitempty"`
RoomName string `protobuf:"bytes,2,opt,name=room_name,json=roomName,proto3" json:"room_name,omitempty"`
RoomAvatar string `protobuf:"bytes,3,opt,name=room_avatar,json=roomAvatar,proto3" json:"room_avatar,omitempty"`
RoomDescription string `protobuf:"bytes,4,opt,name=room_description,json=roomDescription,proto3" json:"room_description,omitempty"`
SeatCount int32 `protobuf:"varint,5,opt,name=seat_count,json=seatCount,proto3" json:"seat_count,omitempty"`
}
func (x *RoomProfileUpdated) Reset() {
*x = RoomProfileUpdated{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RoomProfileUpdated) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RoomProfileUpdated) ProtoMessage() {}
func (x *RoomProfileUpdated) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
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 RoomProfileUpdated.ProtoReflect.Descriptor instead.
func (*RoomProfileUpdated) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{2}
}
func (x *RoomProfileUpdated) GetActorUserId() int64 {
if x != nil {
return x.ActorUserId
}
return 0
}
func (x *RoomProfileUpdated) GetRoomName() string {
if x != nil {
return x.RoomName
}
return ""
}
func (x *RoomProfileUpdated) GetRoomAvatar() string {
if x != nil {
return x.RoomAvatar
}
return ""
}
func (x *RoomProfileUpdated) GetRoomDescription() string {
if x != nil {
return x.RoomDescription
}
return ""
}
func (x *RoomProfileUpdated) GetSeatCount() int32 {
if x != nil {
return x.SeatCount
}
return 0
}
// RoomUserJoined 表达用户业务态进房成功。
type RoomUserJoined struct {
state protoimpl.MessageState
@ -222,7 +300,7 @@ type RoomUserJoined struct {
func (x *RoomUserJoined) Reset() {
*x = RoomUserJoined{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -234,7 +312,7 @@ func (x *RoomUserJoined) String() string {
func (*RoomUserJoined) ProtoMessage() {}
func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[2]
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -247,7 +325,7 @@ func (x *RoomUserJoined) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomUserJoined.ProtoReflect.Descriptor instead.
func (*RoomUserJoined) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{2}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{3}
}
func (x *RoomUserJoined) GetUserId() int64 {
@ -275,7 +353,7 @@ type RoomUserLeft struct {
func (x *RoomUserLeft) Reset() {
*x = RoomUserLeft{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -287,7 +365,7 @@ func (x *RoomUserLeft) String() string {
func (*RoomUserLeft) ProtoMessage() {}
func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[3]
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -300,7 +378,7 @@ func (x *RoomUserLeft) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomUserLeft.ProtoReflect.Descriptor instead.
func (*RoomUserLeft) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{3}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{4}
}
func (x *RoomUserLeft) GetUserId() int64 {
@ -322,7 +400,7 @@ type RoomClosed struct {
func (x *RoomClosed) Reset() {
*x = RoomClosed{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -334,7 +412,7 @@ func (x *RoomClosed) String() string {
func (*RoomClosed) ProtoMessage() {}
func (x *RoomClosed) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -347,7 +425,7 @@ func (x *RoomClosed) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomClosed.ProtoReflect.Descriptor instead.
func (*RoomClosed) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{4}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{5}
}
func (x *RoomClosed) GetActorUserId() int64 {
@ -387,7 +465,7 @@ type RoomMicChanged struct {
func (x *RoomMicChanged) Reset() {
*x = RoomMicChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -399,7 +477,7 @@ func (x *RoomMicChanged) String() string {
func (*RoomMicChanged) ProtoMessage() {}
func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[5]
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -412,7 +490,7 @@ func (x *RoomMicChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomMicChanged.ProtoReflect.Descriptor instead.
func (*RoomMicChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{5}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{6}
}
func (x *RoomMicChanged) GetActorUserId() int64 {
@ -498,7 +576,7 @@ type RoomMicSeatLocked struct {
func (x *RoomMicSeatLocked) Reset() {
*x = RoomMicSeatLocked{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -510,7 +588,7 @@ func (x *RoomMicSeatLocked) String() string {
func (*RoomMicSeatLocked) ProtoMessage() {}
func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[6]
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -523,7 +601,7 @@ func (x *RoomMicSeatLocked) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomMicSeatLocked.ProtoReflect.Descriptor instead.
func (*RoomMicSeatLocked) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{6}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{7}
}
func (x *RoomMicSeatLocked) GetActorUserId() int64 {
@ -559,7 +637,7 @@ type RoomChatEnabledChanged struct {
func (x *RoomChatEnabledChanged) Reset() {
*x = RoomChatEnabledChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -571,7 +649,7 @@ func (x *RoomChatEnabledChanged) String() string {
func (*RoomChatEnabledChanged) ProtoMessage() {}
func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[7]
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -584,7 +662,7 @@ func (x *RoomChatEnabledChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomChatEnabledChanged.ProtoReflect.Descriptor instead.
func (*RoomChatEnabledChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{7}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{8}
}
func (x *RoomChatEnabledChanged) GetActorUserId() int64 {
@ -614,7 +692,7 @@ type RoomAdminChanged struct {
func (x *RoomAdminChanged) Reset() {
*x = RoomAdminChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -626,7 +704,7 @@ func (x *RoomAdminChanged) String() string {
func (*RoomAdminChanged) ProtoMessage() {}
func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[8]
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -639,7 +717,7 @@ func (x *RoomAdminChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomAdminChanged.ProtoReflect.Descriptor instead.
func (*RoomAdminChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{8}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{9}
}
func (x *RoomAdminChanged) GetActorUserId() int64 {
@ -676,7 +754,7 @@ type RoomHostTransferred struct {
func (x *RoomHostTransferred) Reset() {
*x = RoomHostTransferred{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -688,7 +766,7 @@ func (x *RoomHostTransferred) String() string {
func (*RoomHostTransferred) ProtoMessage() {}
func (x *RoomHostTransferred) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[9]
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -701,7 +779,7 @@ func (x *RoomHostTransferred) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomHostTransferred.ProtoReflect.Descriptor instead.
func (*RoomHostTransferred) 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 *RoomHostTransferred) GetActorUserId() int64 {
@ -738,7 +816,7 @@ type RoomUserMuted struct {
func (x *RoomUserMuted) Reset() {
*x = RoomUserMuted{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -750,7 +828,7 @@ func (x *RoomUserMuted) String() string {
func (*RoomUserMuted) ProtoMessage() {}
func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[10]
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -763,7 +841,7 @@ func (x *RoomUserMuted) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomUserMuted.ProtoReflect.Descriptor instead.
func (*RoomUserMuted) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11}
}
func (x *RoomUserMuted) GetActorUserId() int64 {
@ -799,7 +877,7 @@ type RoomUserKicked struct {
func (x *RoomUserKicked) Reset() {
*x = RoomUserKicked{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -811,7 +889,7 @@ func (x *RoomUserKicked) String() string {
func (*RoomUserKicked) ProtoMessage() {}
func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[11]
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -824,7 +902,7 @@ func (x *RoomUserKicked) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomUserKicked.ProtoReflect.Descriptor instead.
func (*RoomUserKicked) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12}
}
func (x *RoomUserKicked) GetActorUserId() int64 {
@ -853,7 +931,7 @@ type RoomUserUnbanned struct {
func (x *RoomUserUnbanned) Reset() {
*x = RoomUserUnbanned{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -865,7 +943,7 @@ func (x *RoomUserUnbanned) String() string {
func (*RoomUserUnbanned) ProtoMessage() {}
func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[12]
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -878,7 +956,7 @@ func (x *RoomUserUnbanned) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomUserUnbanned.ProtoReflect.Descriptor instead.
func (*RoomUserUnbanned) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13}
}
func (x *RoomUserUnbanned) GetActorUserId() int64 {
@ -913,7 +991,7 @@ type RoomGiftSent struct {
func (x *RoomGiftSent) Reset() {
*x = RoomGiftSent{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -925,7 +1003,7 @@ func (x *RoomGiftSent) String() string {
func (*RoomGiftSent) ProtoMessage() {}
func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[13]
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -938,7 +1016,7 @@ func (x *RoomGiftSent) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomGiftSent.ProtoReflect.Descriptor instead.
func (*RoomGiftSent) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14}
}
func (x *RoomGiftSent) GetSenderUserId() int64 {
@ -1009,7 +1087,7 @@ type RoomHeatChanged struct {
func (x *RoomHeatChanged) Reset() {
*x = RoomHeatChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1021,7 +1099,7 @@ func (x *RoomHeatChanged) String() string {
func (*RoomHeatChanged) ProtoMessage() {}
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[14]
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1034,7 +1112,7 @@ func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomHeatChanged.ProtoReflect.Descriptor instead.
func (*RoomHeatChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15}
}
func (x *RoomHeatChanged) GetDelta() int64 {
@ -1064,7 +1142,7 @@ type RoomRankChanged struct {
func (x *RoomRankChanged) Reset() {
*x = RoomRankChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1076,7 +1154,7 @@ func (x *RoomRankChanged) String() string {
func (*RoomRankChanged) ProtoMessage() {}
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[15]
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1089,7 +1167,7 @@ func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomRankChanged.ProtoReflect.Descriptor instead.
func (*RoomRankChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{16}
}
func (x *RoomRankChanged) GetUserId() int64 {
@ -1148,122 +1226,134 @@ var file_proto_events_room_v1_events_proto_rawDesc = []byte{
0x09, 0x52, 0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x41, 0x76, 0x61, 0x74, 0x61, 0x72, 0x12, 0x29, 0x0a,
0x10, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f,
0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x6f, 0x6f, 0x6d, 0x44, 0x65, 0x73,
0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3d, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d,
0x55, 0x73, 0x65, 0x72, 0x4a, 0x6f, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x27, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x55,
0x73, 0x65, 0x72, 0x4c, 0x65, 0x66, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64,
0x22, 0x48, 0x0a, 0x0a, 0x52, 0x6f, 0x6f, 0x6d, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x64, 0x12, 0x22,
0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72,
0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xee, 0x02, 0x0a, 0x0e, 0x52,
0x6f, 0x6f, 0x6d, 0x4d, 0x69, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x22, 0x0a,
0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65,
0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72, 0x6f, 0x6d, 0x5f,
0x73, 0x65, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66, 0x72, 0x6f, 0x6d,
0x53, 0x65, 0x61, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x6f, 0x5f, 0x73, 0x65, 0x61, 0x74, 0x18,
0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x74, 0x6f, 0x53, 0x65, 0x61, 0x74, 0x12, 0x16, 0x0a,
0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61,
0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x69, 0x63, 0x5f, 0x73, 0x65, 0x73,
0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6d,
0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x07, 0x20, 0x01,
0x28, 0x09, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x53, 0x74, 0x61, 0x74, 0x65,
0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09,
0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x70, 0x75, 0x62, 0x6c,
0x69, 0x73, 0x68, 0x5f, 0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x6d, 0x73, 0x18,
0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x44, 0x65,
0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x4d, 0x73, 0x12, 0x31, 0x0a, 0x15, 0x70, 0x75, 0x62, 0x6c,
0x69, 0x73, 0x68, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x6d,
0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68,
0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x22, 0x68, 0x0a, 0x11, 0x52,
0x6f, 0x6f, 0x6d, 0x4d, 0x69, 0x63, 0x53, 0x65, 0x61, 0x74, 0x4c, 0x6f, 0x63, 0x6b, 0x65, 0x64,
0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73,
0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65, 0x61, 0x74, 0x5f, 0x6e, 0x6f, 0x18,
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x65, 0x61, 0x74, 0x4e, 0x6f, 0x12, 0x16, 0x0a,
0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x6c,
0x6f, 0x63, 0x6b, 0x65, 0x64, 0x22, 0x56, 0x0a, 0x16, 0x52, 0x6f, 0x6f, 0x6d, 0x43, 0x68, 0x61,
0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12,
0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xc0, 0x01, 0x0a, 0x12, 0x52, 0x6f, 0x6f,
0x6d, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x12,
0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64,
0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x02,
0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x76, 0x0a,
0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x6f, 0x6f, 0x6d, 0x4e, 0x61, 0x6d, 0x65,
0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x6f, 0x6f, 0x6d, 0x41, 0x76, 0x61, 0x74, 0x61,
0x72, 0x12, 0x29, 0x0a, 0x10, 0x72, 0x6f, 0x6f, 0x6d, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69,
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x72, 0x6f, 0x6f,
0x6d, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x0a, 0x0a,
0x73, 0x65, 0x61, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05,
0x52, 0x09, 0x73, 0x65, 0x61, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x3d, 0x0a, 0x0e, 0x52,
0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4a, 0x6f, 0x69, 0x6e, 0x65, 0x64, 0x12, 0x17, 0x0a,
0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x72, 0x6f, 0x6c, 0x65, 0x22, 0x27, 0x0a, 0x0c, 0x52, 0x6f,
0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4c, 0x65, 0x66, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65,
0x72, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x0a, 0x52, 0x6f, 0x6f, 0x6d, 0x43, 0x6c, 0x6f, 0x73, 0x65,
0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f,
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55,
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65,
0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e,
0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x8b, 0x01, 0x0a, 0x13, 0x52, 0x6f, 0x6f, 0x6d, 0x48, 0x6f,
0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64, 0x12, 0x22, 0x0a,
0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x27, 0x0a, 0x10, 0x6f, 0x6c, 0x64, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6f, 0x6c, 0x64,
0x48, 0x6f, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x10, 0x6e, 0x65,
0x77, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x03,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6e, 0x65, 0x77, 0x48, 0x6f, 0x73, 0x74, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x22, 0x6f, 0x0a, 0x0d, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x4d,
0x75, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73,
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, 0xee, 0x02,
0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x4d, 0x69, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64,
0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73,
0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75,
0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61,
0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x72,
0x6f, 0x6d, 0x5f, 0x73, 0x65, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x66,
0x72, 0x6f, 0x6d, 0x53, 0x65, 0x61, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x6f, 0x5f, 0x73, 0x65,
0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x74, 0x6f, 0x53, 0x65, 0x61, 0x74,
0x12, 0x16, 0x0a, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x06, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x69, 0x63, 0x5f,
0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
0x52, 0x0c, 0x6d, 0x69, 0x63, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x23,
0x0a, 0x0d, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18,
0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x53, 0x74,
0x61, 0x74, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x08, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x2e, 0x0a, 0x13, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x64, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x5f,
0x6d, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73,
0x68, 0x44, 0x65, 0x61, 0x64, 0x6c, 0x69, 0x6e, 0x65, 0x4d, 0x73, 0x12, 0x31, 0x0a, 0x15, 0x70,
0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x69, 0x6d,
0x65, 0x5f, 0x6d, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x12, 0x70, 0x75, 0x62, 0x6c,
0x69, 0x73, 0x68, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x4d, 0x73, 0x22, 0x68,
0x0a, 0x11, 0x52, 0x6f, 0x6f, 0x6d, 0x4d, 0x69, 0x63, 0x53, 0x65, 0x61, 0x74, 0x4c, 0x6f, 0x63,
0x6b, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f,
0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x73, 0x65, 0x61, 0x74, 0x5f,
0x6e, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x65, 0x61, 0x74, 0x4e, 0x6f,
0x12, 0x16, 0x0a, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x52, 0x06, 0x6c, 0x6f, 0x63, 0x6b, 0x65, 0x64, 0x22, 0x56, 0x0a, 0x16, 0x52, 0x6f, 0x6f, 0x6d,
0x43, 0x68, 0x61, 0x74, 0x45, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x43, 0x68, 0x61, 0x6e, 0x67,
0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72,
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65,
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64,
0x22, 0x76, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x43, 0x68, 0x61,
0x6e, 0x67, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74,
0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14,
0x0a, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x05, 0x6d,
0x75, 0x74, 0x65, 0x64, 0x22, 0x5a, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72,
0x4b, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61,
0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61,
0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64,
0x22, 0x5c, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x55, 0x6e, 0x62, 0x61,
0x6e, 0x6e, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74,
0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x18,
0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52,
0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x22, 0x8b, 0x01, 0x0a, 0x13, 0x52, 0x6f, 0x6f,
0x6d, 0x48, 0x6f, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x72, 0x65, 0x64,
0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73,
0x65, 0x72, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x10, 0x6f, 0x6c, 0x64, 0x5f, 0x68, 0x6f, 0x73, 0x74,
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d,
0x6f, 0x6c, 0x64, 0x48, 0x6f, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x27, 0x0a,
0x10, 0x6e, 0x65, 0x77, 0x5f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6e, 0x65, 0x77, 0x48, 0x6f, 0x73, 0x74,
0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x6f, 0x0a, 0x0d, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73,
0x65, 0x72, 0x4d, 0x75, 0x74, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72,
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08,
0x52, 0x05, 0x6d, 0x75, 0x74, 0x65, 0x64, 0x22, 0x5a, 0x0a, 0x0e, 0x52, 0x6f, 0x6f, 0x6d, 0x55,
0x73, 0x65, 0x72, 0x4b, 0x69, 0x63, 0x6b, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74,
0x6f, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0b, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a,
0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65,
0x72, 0x49, 0x64, 0x22, 0x5c, 0x0a, 0x10, 0x52, 0x6f, 0x6f, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x55,
0x6e, 0x62, 0x61, 0x6e, 0x6e, 0x65, 0x64, 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72,
0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b,
0x61, 0x63, 0x74, 0x6f, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x03, 0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49,
0x64, 0x22, 0xaa, 0x02, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65,
0x6e, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65,
0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64,
0x65, 0x72, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67,
0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03,
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0xaa,
0x02, 0x0a, 0x0c, 0x52, 0x6f, 0x6f, 0x6d, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x6e, 0x74, 0x12,
0x24, 0x0a, 0x0e, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x73, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x55,
0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x5f,
0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x74,
0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x67,
0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x67, 0x69,
0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x43, 0x6f,
0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75,
0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c,
0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x72, 0x65,
0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10,
0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x49, 0x64,
0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72, 0x65, 0x67, 0x69,
0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x76, 0x69, 0x73,
0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a,
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09,
0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x22, 0x4a, 0x0a, 0x0f, 0x52,
0x6f, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x14,
0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x64,
0x65, 0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x5f,
0x68, 0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63, 0x75, 0x72, 0x72,
0x65, 0x6e, 0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x52,
0x61, 0x6e, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73,
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x75, 0x73, 0x65,
0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01,
0x28, 0x03, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66,
0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67,
0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x68, 0x79, 0x61, 0x70,
0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x72, 0x6f, 0x6f, 0x6d, 0x2f, 0x76, 0x31,
0x3b, 0x72, 0x6f, 0x6f, 0x6d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x76, 0x31, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
0x52, 0x0c, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x17,
0x0a, 0x07, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
0x06, 0x67, 0x69, 0x66, 0x74, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x67, 0x69, 0x66,
0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x67, 0x69, 0x66, 0x74, 0x5f, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x67, 0x69, 0x66, 0x74,
0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67,
0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28,
0x09, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70,
0x74, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11, 0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x72,
0x65, 0x67, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f,
0x76, 0x69, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x67, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12,
0x1d, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20,
0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x22, 0x4a,
0x0a, 0x0f, 0x52, 0x6f, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
0x64, 0x12, 0x14, 0x0a, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03,
0x52, 0x05, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x75, 0x72, 0x72, 0x65,
0x6e, 0x74, 0x5f, 0x68, 0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x63,
0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x65, 0x61, 0x74, 0x22, 0x5f, 0x0a, 0x0f, 0x52, 0x6f,
0x6f, 0x6d, 0x52, 0x61, 0x6e, 0x6b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x64, 0x12, 0x17, 0x0a,
0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06,
0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18,
0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a,
0x67, 0x69, 0x66, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03,
0x52, 0x09, 0x67, 0x69, 0x66, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x68,
0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x72, 0x6f, 0x6f, 0x6d,
0x2f, 0x76, 0x31, 0x3b, 0x72, 0x6f, 0x6f, 0x6d, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x76, 0x31,
0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@ -1278,24 +1368,25 @@ func file_proto_events_room_v1_events_proto_rawDescGZIP() []byte {
return file_proto_events_room_v1_events_proto_rawDescData
}
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 17)
var file_proto_events_room_v1_events_proto_goTypes = []any{
(*EventEnvelope)(nil), // 0: hyapp.events.room.v1.EventEnvelope
(*RoomCreated)(nil), // 1: hyapp.events.room.v1.RoomCreated
(*RoomUserJoined)(nil), // 2: hyapp.events.room.v1.RoomUserJoined
(*RoomUserLeft)(nil), // 3: hyapp.events.room.v1.RoomUserLeft
(*RoomClosed)(nil), // 4: hyapp.events.room.v1.RoomClosed
(*RoomMicChanged)(nil), // 5: hyapp.events.room.v1.RoomMicChanged
(*RoomMicSeatLocked)(nil), // 6: hyapp.events.room.v1.RoomMicSeatLocked
(*RoomChatEnabledChanged)(nil), // 7: hyapp.events.room.v1.RoomChatEnabledChanged
(*RoomAdminChanged)(nil), // 8: hyapp.events.room.v1.RoomAdminChanged
(*RoomHostTransferred)(nil), // 9: hyapp.events.room.v1.RoomHostTransferred
(*RoomUserMuted)(nil), // 10: hyapp.events.room.v1.RoomUserMuted
(*RoomUserKicked)(nil), // 11: hyapp.events.room.v1.RoomUserKicked
(*RoomUserUnbanned)(nil), // 12: hyapp.events.room.v1.RoomUserUnbanned
(*RoomGiftSent)(nil), // 13: hyapp.events.room.v1.RoomGiftSent
(*RoomHeatChanged)(nil), // 14: hyapp.events.room.v1.RoomHeatChanged
(*RoomRankChanged)(nil), // 15: hyapp.events.room.v1.RoomRankChanged
(*RoomProfileUpdated)(nil), // 2: hyapp.events.room.v1.RoomProfileUpdated
(*RoomUserJoined)(nil), // 3: hyapp.events.room.v1.RoomUserJoined
(*RoomUserLeft)(nil), // 4: hyapp.events.room.v1.RoomUserLeft
(*RoomClosed)(nil), // 5: hyapp.events.room.v1.RoomClosed
(*RoomMicChanged)(nil), // 6: hyapp.events.room.v1.RoomMicChanged
(*RoomMicSeatLocked)(nil), // 7: hyapp.events.room.v1.RoomMicSeatLocked
(*RoomChatEnabledChanged)(nil), // 8: hyapp.events.room.v1.RoomChatEnabledChanged
(*RoomAdminChanged)(nil), // 9: hyapp.events.room.v1.RoomAdminChanged
(*RoomHostTransferred)(nil), // 10: hyapp.events.room.v1.RoomHostTransferred
(*RoomUserMuted)(nil), // 11: hyapp.events.room.v1.RoomUserMuted
(*RoomUserKicked)(nil), // 12: hyapp.events.room.v1.RoomUserKicked
(*RoomUserUnbanned)(nil), // 13: hyapp.events.room.v1.RoomUserUnbanned
(*RoomGiftSent)(nil), // 14: hyapp.events.room.v1.RoomGiftSent
(*RoomHeatChanged)(nil), // 15: hyapp.events.room.v1.RoomHeatChanged
(*RoomRankChanged)(nil), // 16: hyapp.events.room.v1.RoomRankChanged
}
var file_proto_events_room_v1_events_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
@ -1316,7 +1407,7 @@ func file_proto_events_room_v1_events_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_proto_events_room_v1_events_proto_rawDesc,
NumEnums: 0,
NumMessages: 16,
NumMessages: 17,
NumExtensions: 0,
NumServices: 0,
},

View File

@ -28,6 +28,15 @@ message RoomCreated {
string room_description = 7;
}
// RoomProfileUpdated
message RoomProfileUpdated {
int64 actor_user_id = 1;
string room_name = 2;
string room_avatar = 3;
string room_description = 4;
int32 seat_count = 5;
}
// RoomUserJoined
message RoomUserJoined {
int64 user_id = 1;

File diff suppressed because it is too large Load Diff

View File

@ -81,11 +81,11 @@ message RoomSnapshot {
}
// CreateRoomRequest
// meta.room_idmeta.command_idmeta.actor_user_idseat_countmode room_name
// meta.room_idmeta.command_idmeta.actor_user_idmode room_name
// meta.actor_user_id owner Conflict
message CreateRoomRequest {
RequestMeta meta = 1;
// seat_count 0 Room Cell
// seat_count 0 使
int32 seat_count = 2;
// mode
string mode = 3;
@ -107,6 +107,22 @@ message CreateRoomResponse {
RoomSnapshot room = 2;
}
// UpdateRoomProfileRequest
// presence owner
message UpdateRoomProfileRequest {
RequestMeta meta = 1;
optional string room_name = 2;
optional string room_avatar = 3;
optional string room_description = 4;
optional int32 seat_count = 5;
}
// UpdateRoomProfileResponse
message UpdateRoomProfileResponse {
CommandResult result = 1;
RoomSnapshot room = 2;
}
// JoinRoomRequest presence
message JoinRoomRequest {
RequestMeta meta = 1;
@ -480,6 +496,7 @@ message GetRoomSnapshotResponse {
// RoomCommandService
service RoomCommandService {
rpc CreateRoom(CreateRoomRequest) returns (CreateRoomResponse);
rpc UpdateRoomProfile(UpdateRoomProfileRequest) returns (UpdateRoomProfileResponse);
rpc JoinRoom(JoinRoomRequest) returns (JoinRoomResponse);
rpc RoomHeartbeat(RoomHeartbeatRequest) returns (RoomHeartbeatResponse);
rpc LeaveRoom(LeaveRoomRequest) returns (LeaveRoomResponse);

View File

@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9
const (
RoomCommandService_CreateRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/CreateRoom"
RoomCommandService_UpdateRoomProfile_FullMethodName = "/hyapp.room.v1.RoomCommandService/UpdateRoomProfile"
RoomCommandService_JoinRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/JoinRoom"
RoomCommandService_RoomHeartbeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/RoomHeartbeat"
RoomCommandService_LeaveRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/LeaveRoom"
@ -46,6 +47,7 @@ const (
// RoomCommandService 承载所有会改变房间状态的命令。
type RoomCommandServiceClient interface {
CreateRoom(ctx context.Context, in *CreateRoomRequest, opts ...grpc.CallOption) (*CreateRoomResponse, error)
UpdateRoomProfile(ctx context.Context, in *UpdateRoomProfileRequest, opts ...grpc.CallOption) (*UpdateRoomProfileResponse, error)
JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error)
RoomHeartbeat(ctx context.Context, in *RoomHeartbeatRequest, opts ...grpc.CallOption) (*RoomHeartbeatResponse, error)
LeaveRoom(ctx context.Context, in *LeaveRoomRequest, opts ...grpc.CallOption) (*LeaveRoomResponse, error)
@ -83,6 +85,16 @@ func (c *roomCommandServiceClient) CreateRoom(ctx context.Context, in *CreateRoo
return out, nil
}
func (c *roomCommandServiceClient) UpdateRoomProfile(ctx context.Context, in *UpdateRoomProfileRequest, opts ...grpc.CallOption) (*UpdateRoomProfileResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateRoomProfileResponse)
err := c.cc.Invoke(ctx, RoomCommandService_UpdateRoomProfile_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *roomCommandServiceClient) JoinRoom(ctx context.Context, in *JoinRoomRequest, opts ...grpc.CallOption) (*JoinRoomResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(JoinRoomResponse)
@ -260,6 +272,7 @@ func (c *roomCommandServiceClient) SendGift(ctx context.Context, in *SendGiftReq
// RoomCommandService 承载所有会改变房间状态的命令。
type RoomCommandServiceServer interface {
CreateRoom(context.Context, *CreateRoomRequest) (*CreateRoomResponse, error)
UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error)
JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error)
RoomHeartbeat(context.Context, *RoomHeartbeatRequest) (*RoomHeartbeatResponse, error)
LeaveRoom(context.Context, *LeaveRoomRequest) (*LeaveRoomResponse, error)
@ -290,6 +303,9 @@ type UnimplementedRoomCommandServiceServer struct{}
func (UnimplementedRoomCommandServiceServer) CreateRoom(context.Context, *CreateRoomRequest) (*CreateRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateRoom not implemented")
}
func (UnimplementedRoomCommandServiceServer) UpdateRoomProfile(context.Context, *UpdateRoomProfileRequest) (*UpdateRoomProfileResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateRoomProfile not implemented")
}
func (UnimplementedRoomCommandServiceServer) JoinRoom(context.Context, *JoinRoomRequest) (*JoinRoomResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method JoinRoom not implemented")
}
@ -380,6 +396,24 @@ func _RoomCommandService_CreateRoom_Handler(srv interface{}, ctx context.Context
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_UpdateRoomProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateRoomProfileRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(RoomCommandServiceServer).UpdateRoomProfile(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: RoomCommandService_UpdateRoomProfile_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(RoomCommandServiceServer).UpdateRoomProfile(ctx, req.(*UpdateRoomProfileRequest))
}
return interceptor(ctx, in, info, handler)
}
func _RoomCommandService_JoinRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(JoinRoomRequest)
if err := dec(in); err != nil {
@ -697,6 +731,10 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
MethodName: "CreateRoom",
Handler: _RoomCommandService_CreateRoom_Handler,
},
{
MethodName: "UpdateRoomProfile",
Handler: _RoomCommandService_UpdateRoomProfile_Handler,
},
{
MethodName: "JoinRoom",
Handler: _RoomCommandService_JoinRoom_Handler,

File diff suppressed because it is too large Load Diff

View File

@ -251,6 +251,17 @@ message GetUserRoleSummaryResponse {
UserRoleSummary summary = 1;
}
message CheckBusinessCapabilityRequest {
RequestMeta meta = 1;
int64 actor_user_id = 2;
string capability = 3;
}
message CheckBusinessCapabilityResponse {
bool allowed = 1;
string reason = 2;
}
message GetAgencyMembersRequest {
RequestMeta meta = 1;
int64 agency_id = 2;
@ -391,6 +402,7 @@ service UserHostService {
rpc GetBDProfile(GetBDProfileRequest) returns (GetBDProfileResponse);
rpc GetCoinSellerProfile(GetCoinSellerProfileRequest) returns (GetCoinSellerProfileResponse);
rpc GetUserRoleSummary(GetUserRoleSummaryRequest) returns (GetUserRoleSummaryResponse);
rpc CheckBusinessCapability(CheckBusinessCapabilityRequest) returns (CheckBusinessCapabilityResponse);
rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse);
rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse);
}

View File

@ -30,6 +30,7 @@ const (
UserHostService_GetBDProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetBDProfile"
UserHostService_GetCoinSellerProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetCoinSellerProfile"
UserHostService_GetUserRoleSummary_FullMethodName = "/hyapp.user.v1.UserHostService/GetUserRoleSummary"
UserHostService_CheckBusinessCapability_FullMethodName = "/hyapp.user.v1.UserHostService/CheckBusinessCapability"
UserHostService_GetAgencyMembers_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyMembers"
UserHostService_GetAgencyApplications_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyApplications"
)
@ -51,6 +52,7 @@ type UserHostServiceClient interface {
GetBDProfile(ctx context.Context, in *GetBDProfileRequest, opts ...grpc.CallOption) (*GetBDProfileResponse, error)
GetCoinSellerProfile(ctx context.Context, in *GetCoinSellerProfileRequest, opts ...grpc.CallOption) (*GetCoinSellerProfileResponse, error)
GetUserRoleSummary(ctx context.Context, in *GetUserRoleSummaryRequest, opts ...grpc.CallOption) (*GetUserRoleSummaryResponse, error)
CheckBusinessCapability(ctx context.Context, in *CheckBusinessCapabilityRequest, opts ...grpc.CallOption) (*CheckBusinessCapabilityResponse, error)
GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error)
GetAgencyApplications(ctx context.Context, in *GetAgencyApplicationsRequest, opts ...grpc.CallOption) (*GetAgencyApplicationsResponse, error)
}
@ -173,6 +175,16 @@ func (c *userHostServiceClient) GetUserRoleSummary(ctx context.Context, in *GetU
return out, nil
}
func (c *userHostServiceClient) CheckBusinessCapability(ctx context.Context, in *CheckBusinessCapabilityRequest, opts ...grpc.CallOption) (*CheckBusinessCapabilityResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CheckBusinessCapabilityResponse)
err := c.cc.Invoke(ctx, UserHostService_CheckBusinessCapability_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userHostServiceClient) GetAgencyMembers(ctx context.Context, in *GetAgencyMembersRequest, opts ...grpc.CallOption) (*GetAgencyMembersResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAgencyMembersResponse)
@ -210,6 +222,7 @@ type UserHostServiceServer interface {
GetBDProfile(context.Context, *GetBDProfileRequest) (*GetBDProfileResponse, error)
GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error)
GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error)
CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error)
GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error)
GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error)
mustEmbedUnimplementedUserHostServiceServer()
@ -255,6 +268,9 @@ func (UnimplementedUserHostServiceServer) GetCoinSellerProfile(context.Context,
func (UnimplementedUserHostServiceServer) GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUserRoleSummary not implemented")
}
func (UnimplementedUserHostServiceServer) CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckBusinessCapability not implemented")
}
func (UnimplementedUserHostServiceServer) GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAgencyMembers not implemented")
}
@ -480,6 +496,24 @@ func _UserHostService_GetUserRoleSummary_Handler(srv interface{}, ctx context.Co
return interceptor(ctx, in, info, handler)
}
func _UserHostService_CheckBusinessCapability_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CheckBusinessCapabilityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserHostServiceServer).CheckBusinessCapability(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserHostService_CheckBusinessCapability_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserHostServiceServer).CheckBusinessCapability(ctx, req.(*CheckBusinessCapabilityRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserHostService_GetAgencyMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAgencyMembersRequest)
if err := dec(in); err != nil {
@ -567,6 +601,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetUserRoleSummary",
Handler: _UserHostService_GetUserRoleSummary_Handler,
},
{
MethodName: "CheckBusinessCapability",
Handler: _UserHostService_CheckBusinessCapability_Handler,
},
{
MethodName: "GetAgencyMembers",
Handler: _UserHostService_GetAgencyMembers_Handler,

File diff suppressed because it is too large Load Diff

View File

@ -171,6 +171,29 @@ message GetUserResponse {
User user = 1;
}
message BusinessUserLookupRequest {
RequestMeta meta = 1;
int64 actor_user_id = 2;
string scene = 3;
string keyword = 4;
int32 page_size = 5;
}
message BusinessUserLookupItem {
int64 user_id = 1;
string display_user_id = 2;
string username = 3;
string avatar = 4;
UserStatus status = 5;
int64 region_id = 6;
string region_code = 7;
string region_name = 8;
}
message BusinessUserLookupResponse {
repeated BusinessUserLookupItem users = 1;
}
// UserProfileStats read model
message UserProfileStats {
int64 user_id = 1;
@ -627,6 +650,7 @@ message ExpirePrettyDisplayUserIDResponse {
// UserService
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc BusinessUserLookup(BusinessUserLookupRequest) returns (BusinessUserLookupResponse);
rpc GetMyProfileStats(GetMyProfileStatsRequest) returns (GetMyProfileStatsResponse);
rpc BatchGetUsers(BatchGetUsersRequest) returns (BatchGetUsersResponse);
rpc ListUserIDs(ListUserIDsRequest) returns (ListUserIDsResponse);

View File

@ -20,6 +20,7 @@ const _ = grpc.SupportPackageIsVersion9
const (
UserService_GetUser_FullMethodName = "/hyapp.user.v1.UserService/GetUser"
UserService_BusinessUserLookup_FullMethodName = "/hyapp.user.v1.UserService/BusinessUserLookup"
UserService_GetMyProfileStats_FullMethodName = "/hyapp.user.v1.UserService/GetMyProfileStats"
UserService_BatchGetUsers_FullMethodName = "/hyapp.user.v1.UserService/BatchGetUsers"
UserService_ListUserIDs_FullMethodName = "/hyapp.user.v1.UserService/ListUserIDs"
@ -36,6 +37,7 @@ const (
// UserService 提供用户主状态、基础资料和注册资料读写接口。
type UserServiceClient interface {
GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error)
BusinessUserLookup(ctx context.Context, in *BusinessUserLookupRequest, opts ...grpc.CallOption) (*BusinessUserLookupResponse, error)
GetMyProfileStats(ctx context.Context, in *GetMyProfileStatsRequest, opts ...grpc.CallOption) (*GetMyProfileStatsResponse, error)
BatchGetUsers(ctx context.Context, in *BatchGetUsersRequest, opts ...grpc.CallOption) (*BatchGetUsersResponse, error)
ListUserIDs(ctx context.Context, in *ListUserIDsRequest, opts ...grpc.CallOption) (*ListUserIDsResponse, error)
@ -63,6 +65,16 @@ func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opt
return out, nil
}
func (c *userServiceClient) BusinessUserLookup(ctx context.Context, in *BusinessUserLookupRequest, opts ...grpc.CallOption) (*BusinessUserLookupResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BusinessUserLookupResponse)
err := c.cc.Invoke(ctx, UserService_BusinessUserLookup_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userServiceClient) GetMyProfileStats(ctx context.Context, in *GetMyProfileStatsRequest, opts ...grpc.CallOption) (*GetMyProfileStatsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetMyProfileStatsResponse)
@ -140,6 +152,7 @@ func (c *userServiceClient) CompleteOnboarding(ctx context.Context, in *Complete
// UserService 提供用户主状态、基础资料和注册资料读写接口。
type UserServiceServer interface {
GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error)
BusinessUserLookup(context.Context, *BusinessUserLookupRequest) (*BusinessUserLookupResponse, error)
GetMyProfileStats(context.Context, *GetMyProfileStatsRequest) (*GetMyProfileStatsResponse, error)
BatchGetUsers(context.Context, *BatchGetUsersRequest) (*BatchGetUsersResponse, error)
ListUserIDs(context.Context, *ListUserIDsRequest) (*ListUserIDsResponse, error)
@ -160,6 +173,9 @@ type UnimplementedUserServiceServer struct{}
func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented")
}
func (UnimplementedUserServiceServer) BusinessUserLookup(context.Context, *BusinessUserLookupRequest) (*BusinessUserLookupResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BusinessUserLookup not implemented")
}
func (UnimplementedUserServiceServer) GetMyProfileStats(context.Context, *GetMyProfileStatsRequest) (*GetMyProfileStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetMyProfileStats not implemented")
}
@ -220,6 +236,24 @@ func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func
return interceptor(ctx, in, info, handler)
}
func _UserService_BusinessUserLookup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BusinessUserLookupRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserServiceServer).BusinessUserLookup(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserService_BusinessUserLookup_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserServiceServer).BusinessUserLookup(ctx, req.(*BusinessUserLookupRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserService_GetMyProfileStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetMyProfileStatsRequest)
if err := dec(in); err != nil {
@ -357,6 +391,10 @@ var UserService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetUser",
Handler: _UserService_GetUser_Handler,
},
{
MethodName: "BusinessUserLookup",
Handler: _UserService_BusinessUserLookup_Handler,
},
{
MethodName: "GetMyProfileStats",
Handler: _UserService_GetMyProfileStats_Handler,

File diff suppressed because it is too large Load Diff

View File

@ -147,6 +147,7 @@ message Resource {
int64 updated_by_user_id = 18;
int64 created_at_ms = 19;
int64 updated_at_ms = 20;
bool manager_grant_enabled = 21;
}
message ResourceGroupItem {
@ -268,6 +269,7 @@ message ListResourcesRequest {
int32 page = 6;
int32 page_size = 7;
bool active_only = 8;
bool manager_grant_only = 9;
}
message ListResourcesResponse {
@ -303,6 +305,7 @@ message CreateResourceRequest {
string metadata_json = 15;
int32 sort_order = 16;
int64 operator_user_id = 17;
optional bool manager_grant_enabled = 18;
}
message UpdateResourceRequest {
@ -324,6 +327,7 @@ message UpdateResourceRequest {
string metadata_json = 16;
int32 sort_order = 17;
int64 operator_user_id = 18;
optional bool manager_grant_enabled = 19;
}
message SetResourceStatusRequest {

View File

@ -50,7 +50,7 @@ stateDiagram-v2
- `RoomHeartbeat` 只能刷新已有 presence不能创建 presence。
- `LeaveRoom` 要同步释放用户占用的麦位。
- RTC `room_exited` 可以清理 presence但如果用户已经在事件时间之后重新刷新 presence旧事件必须忽略
- RTC `room_exited` 只释放当前麦位,不清理业务 presence用户可能仍停留在房间页等待重连
- `KickUser` 写入 ban 集合,用户必须先 `UnbanUser` 才能重新 `JoinRoom`
## Mic

View File

@ -0,0 +1,314 @@
# Manager Center Resource Grant Architecture
本文定义后台资源列表的“赠送权限”开关、H5 经理中心资源赠送、以及可复用用户查询接口的服务边界。资源 catalog、用户资源权益和赠送事实仍按 [Resource And Resource Group Architecture](./资源组与礼物架构.md) 归属 `wallet-service`经理、Agency、BD 等业务身份仍按 [Host, Agency And BD App Architecture](./主播公会BD架构.md) 归属 `user-service`
## Goals
- 后台资源列表增加“赠送权限” switch新建资源默认打开关闭后资源仍可存在、可用于其他业务但不能出现在经理中心赠送列表也不能被经理中心赠送。
- `server/admin` 和后台前端支持读取、创建、编辑和切换该字段,但不成为资源配置事实 owner。
- H5 经理中心提供资源赠送列表、赠送给用户接口和通用用户查询入口。
- 经理身份必须由服务端校验,客户端不能自报 `is_manager`
- 用户查询接口按业务场景复用首个场景是经理中心赠送后续代理中心、申请成为主播、BD 中心可以复用同一个 user-service 查询能力,但必须传入明确 scene 并执行各自权限和字段裁剪。
## Non-Goals
- 不新增独立 `manager-service``resource-service`
- 不把用户资源余额、背包、赠送记录写入 `server/admin` 数据库。
- 不允许 H5 客户端提交资源价格、金币数量、权益有效期或赠送策略。
- 不支持经理中心赠送资源组;资源组仍只允许后台或明确的系统命令赠送。
- 不在 room-service 中校验经理身份或资源赠送权限。
- 不把通用用户查询做成无权限全局搜索;所有查询必须带 scene且返回字段按 scene 收敛。
## Service Ownership
| Service | Owns | Does Not Own |
| --- | --- | --- |
| `wallet-service` | `resources.manager_grant_enabled`、资源赠送白名单判断、资源赠送事务、钱包入账、用户资源权益、resource outbox | 用户是否经理、用户资料搜索、后台账号权限 |
| `user-service` | 用户主数据、短号解析、通用用户查询、经理/Agency/BD/主播/币商等业务身份能力判定 | 资源 catalog、钱包余额、用户资源权益 |
| `gateway-service` | H5 HTTP `/api/v1` envelope、App 鉴权、scene 参数、调用 user-service 校验能力和目标用户、调用 wallet-service 赠送命令 | 资源事实、用户身份事实、钱包/背包落账 |
| `server/admin` | 后台鉴权、资源列表/编辑表单、操作审计、调用 wallet-service 管理 RPC | 资源事实、H5 经理身份、用户资源背包 |
| H5 经理中心 | 展示资源列表、选择目标用户、提交赠送命令 | 权限判断、目标用户真实性、赠送数量和有效期计算 |
## Core Concepts
| Concept | Meaning |
| --- | --- |
| `manager_grant_enabled` | 资源是否允许由经理中心赠送。它不是后台能否赠送的开关;后台仍使用现有 `grantable` 和后台权限控制。 |
| `manager_center` | App H5 业务入口,由 gateway 鉴权后进入。首版经理身份映射为 active Agency owner后续如果引入 Agency manager 表,只扩展 user-service 能力判定。 |
| `business user lookup` | 通用用户查询能力。调用方必须传 `scene`user-service 按 scene 校验 actor 能力并裁剪返回字段。 |
| `manager resource grant` | 经理中心发起的单资源赠送命令。落库仍是 `resource_grants``grant_source=manager_center``operator_user_id=actor_user_id`。 |
## Admin Resource Switch
`resources` 增加字段:
```sql
ALTER TABLE resources
ADD COLUMN manager_grant_enabled BOOLEAN NOT NULL DEFAULT TRUE,
ADD KEY idx_resources_manager_grant (app_code, manager_grant_enabled, status, sort_order);
```
字段语义:
- 默认值为 `TRUE`,满足“后台资源列表赠送权限 switch 默认打开”。
- 关闭 switch 只影响经理中心赠送列表和经理中心赠送命令。
- `status=disabled` 的资源即使 `manager_grant_enabled=TRUE`,也不能出现在 H5 赠送列表。
- `grantable=FALSE` 的资源即使 `manager_grant_enabled=TRUE`,也不能被任何赠送命令发放。
- 历史已经赠送出去的权益不受该字段变更影响。
后台接口保持当前 `server/admin -> wallet-service` 边界:
```http
GET /api/v1/admin/resources
POST /api/v1/admin/resources
PUT /api/v1/admin/resources/{resource_id}
POST /api/v1/admin/resources/{resource_id}/enable
POST /api/v1/admin/resources/{resource_id}/disable
```
DTO/RPC 需要补充:
- `wallet.v1.Resource.manager_grant_enabled`
- `CreateResourceRequest.manager_grant_enabled`
- `UpdateResourceRequest.manager_grant_enabled`
- `ListResourcesRequest.manager_grant_only`
`server/admin` 只做字段透传和操作审计。新增或编辑资源时,如果请求体不传 `managerGrantEnabled`admin-server 必须向 wallet-service 传 `true`,避免前端漏字段导致默认关闭。
## Manager Capability Boundary
首版经理身份定义为:
- 当前用户拥有 active `Agency`,且 `agencies.owner_user_id = actor_user_id`
- `Agency.status=active`
- 该能力由 `user-service` host domain 判断gateway 不能只读 access token 或客户端传参。
建议在 user-service 增加通用能力接口:
```proto
message CheckBusinessCapabilityRequest {
RequestMeta meta = 1;
int64 actor_user_id = 2;
string capability = 3;
}
message CheckBusinessCapabilityResponse {
bool allowed = 1;
string reason = 2;
}
```
首版 capability
| Capability | Allowed When |
| --- | --- |
| `manager_center` | active Agency owner |
| `manager_resource_grant` | active Agency owner |
后续如果出现 Agency 授权管理员,不改 gateway 和 wallet-service只在 user-service host domain 增加 `agency_managers` 或对应 read model并扩展 capability 判定。
## H5 App APIs
所有接口走 gateway `/api/v1` envelope。access token 解析、`RequestMeta.request_id``app_code``client_ip``user_agent` 由 gateway 透传到内部 gRPC。
### 赠送列表
```http
GET /api/v1/manager-center/resource-grants/resources?resource_type=badge&page=1&page_size=20
```
处理规则:
1. gateway 从 token 取 `actor_user_id`
2. gateway 调 user-service `CheckBusinessCapability(capability=manager_resource_grant)`
3. gateway 调 wallet-service `ListResources(active_only=true, manager_grant_only=true, resource_type, page, page_size)`
4. wallet-service 只返回 `status=active AND grantable=TRUE AND manager_grant_enabled=TRUE` 的资源。
响应字段只返回 H5 展示和提交所需字段:
```json
{
"items": [
{
"resource_id": "101",
"resource_type": "badge",
"name": "VIP Badge",
"asset_url": "https://...",
"preview_url": "https://...",
"wallet_asset_type": "",
"wallet_asset_amount": 0,
"sort_order": 10
}
],
"total": 1,
"page": 1,
"page_size": 20
}
```
H5 不能从该接口获得后台审计字段、`created_by_user_id``updated_by_user_id` 或完整 `metadata_json`。如果某类资源需要额外展示配置,必须在 wallet-service 先定义稳定的展示 DTO不能把原始后台 metadata 透出。
### 赠送给用户
```http
POST /api/v1/manager-center/resource-grants
```
请求:
```json
{
"command_id": "mgr-grant-20260511-0001",
"target_user_id": "900001",
"resource_id": "101",
"reason": "manager_center"
}
```
处理规则:
1. gateway 校验当前用户拥有 `manager_resource_grant` capability。
2. gateway 调 user-service `GetUser(target_user_id)`,确认目标用户属于同一 `app_code`、存在、未禁用、未封禁。
3. gateway 不接受客户端提交 `quantity``duration_ms``wallet_asset_amount`
4. gateway 调 wallet-service `GrantResource`
- `grant_source=manager_center`
- `operator_user_id=actor_user_id`
- `target_user_id=resolved target`
- `quantity=1`
- `duration_ms=0`
- `reason=manager_center`
5. wallet-service 在同一事务中校验资源 `status=active``grantable=TRUE``manager_grant_enabled=TRUE`,再按现有资源策略落 `resource_grants``resource_grant_items`、钱包账本或用户资源权益。
wallet-service 不能只依赖赠送列表过滤。`GrantResource` 必须在写事务内根据 `grant_source=manager_center` 重新锁定资源并校验 `manager_grant_enabled`,防止客户端绕过列表直接提交资源 ID。
幂等规则沿用现有 `resource_grants.command_id`
- 同一 `app_code + command_id`、同一 payload 重试返回原 grant。
- 同一 `command_id` 换目标用户或资源返回冲突。
- `request_id` 只做链路追踪,不能当幂等键。
## Common User Lookup
通用用户查询不要挂在经理中心专属模块里。建议 gateway 暴露统一入口:
```http
GET /api/v1/business/users/lookup?scene=manager_resource_grant&keyword=123456&page_size=20
```
user-service 新增或扩展查询 RPC
```proto
message BusinessUserLookupRequest {
RequestMeta meta = 1;
int64 actor_user_id = 2;
string scene = 3;
string keyword = 4;
int32 page_size = 5;
}
message BusinessUserLookupItem {
int64 user_id = 1;
string display_user_id = 2;
string username = 3;
string avatar = 4;
UserStatus status = 5;
int64 region_id = 6;
string region_code = 7;
string region_name = 8;
}
message BusinessUserLookupResponse {
repeated BusinessUserLookupItem users = 1;
}
```
scene 权限矩阵:
| Scene | Actor Requirement | Query Scope |
| --- | --- | --- |
| `manager_resource_grant` | `manager_resource_grant` capability | active users in same `app_code` |
| `agency_application` | logged-in active user | active agencies or agency owner candidates,按后续申请流程定义 |
| `bd_invite_agency` | active BD or BD Leader | same-region active users,不能已有 active Agency owner |
| `bd_invite_bd` | active BD Leader | same-region active users,不能已有 active BD |
首版只实现 `manager_resource_grant`。未实现 scene 必须返回 `INVALID_ARGUMENT`,不能默认降级成全局用户搜索。
查询安全规则:
- `keyword` 必须是短号、用户 ID 或至少 2 个字符的昵称片段。
- 数字 keyword 优先按 current display_user_id 精确匹配,再按 user_id 精确匹配。
- `page_size` 最大 20。
- 只返回必要展示字段,不返回手机号、登录方式、设备、邀请码绑定、钱包余额或三方身份。
- 被禁用、封禁、跨 `app_code` 用户不返回。
- gateway 对 lookup 和 grant 都要做限流lookup 不能成为撞库枚举入口。
## Sequence
```mermaid
sequenceDiagram
participant H5 as H5 Manager Center
participant G as gateway-service
participant U as user-service
participant W as wallet-service
H5->>G: GET /manager-center/resource-grants/resources
G->>U: CheckBusinessCapability(actor, manager_resource_grant)
U-->>G: allowed
G->>W: ListResources(active_only, manager_grant_only)
W-->>G: grantable resources
G-->>H5: resources
H5->>G: GET /business/users/lookup?scene=manager_resource_grant
G->>U: BusinessUserLookup(actor, scene, keyword)
U-->>G: minimal users
G-->>H5: users
H5->>G: POST /manager-center/resource-grants
G->>U: CheckBusinessCapability(actor, manager_resource_grant)
U-->>G: allowed
G->>U: GetUser(target_user_id)
U-->>G: active target
G->>W: GrantResource(source=manager_center)
W-->>G: resource grant
G-->>H5: grant result
```
## Validation Rules
| Rule | Owner | Error |
| --- | --- | --- |
| actor is not active manager | user-service | `PERMISSION_DENIED` |
| lookup scene unknown | user-service | `INVALID_ARGUMENT` |
| target user not found, disabled, banned, or different app | user-service/gateway | `NOT_FOUND` or `PERMISSION_DENIED` |
| resource disabled | wallet-service | `CONFLICT` |
| resource `grantable=FALSE` | wallet-service | `CONFLICT` |
| resource `manager_grant_enabled=FALSE` and source is `manager_center` | wallet-service | `PERMISSION_DENIED` |
| H5 submits quantity or duration | gateway | ignore or reject;首版建议 reject `INVALID_ARGUMENT` |
| duplicate command_id with changed payload | wallet-service | `LEDGER_CONFLICT` |
## Event And Audit
- 后台切换 `manager_grant_enabled` 产出 `ResourceChanged` outbox便于 H5 资源列表缓存刷新。
- 经理中心赠送成功产出 `ResourceGranted` 和必要的 `UserResourceChanged`/`WalletBalanceChanged` 事件。
- `resource_grants.grant_source='manager_center'` 是 App 赠送审计来源。
- `operator_user_id` 记录发起赠送的经理用户 ID不记录后台 admin 用户。
- gateway 应在结构化日志记录 `request_id``actor_user_id``target_user_id``resource_id``grant_id` 和错误 reason。
## Implementation Order
1. `api/proto/wallet/v1/wallet.proto` 增加 `manager_grant_enabled``manager_grant_only`,运行 `make proto`
2. `wallet-service` MySQL 初始化脚本增加 `resources.manager_grant_enabled`repository list/create/update/status/GrantResource 路径全部带字段和校验。
3. `server/admin` resource DTO/request 增加 `managerGrantEnabled`;后台资源列表 switch 默认打开,编辑时透传。
4. `user-service` 增加 `CheckBusinessCapability``BusinessUserLookup`,首版只开放 `manager_resource_grant` scene。
5. `gateway-service` 增加 H5 经理中心三个入口:赠送资源列表、通用用户查询、赠送资源。
6. 补 OpenAPI、前端生成代码和 H5 页面联调。
7. 补测试wallet manager switch、admin 透传、user lookup 权限、gateway H5 流程和幂等冲突。
## Acceptance Criteria
- 新建资源不传 `managerGrantEnabled` 时,后台列表 switch 显示打开。
- 后台关闭某资源赠送权限后H5 经理中心赠送列表不返回该资源。
- H5 绕过列表直接提交关闭权限的 `resource_id`wallet-service 拒绝赠送。
- 非经理用户调用赠送列表、用户查询或赠送接口均返回权限错误。
- 经理用户可以查询目标用户并完成单资源赠送,`resource_grants.grant_source=manager_center``operator_user_id` 为经理用户 ID。
- 同一 `command_id` 重试不重复发放;换目标或资源返回冲突。

View File

@ -208,7 +208,7 @@ Header: Sign: base64(hmacsha256(callback_sign_key, raw_body))
| --- | --- | --- |
| `EventGroupId=2, EventType=203` | `audio_started` | 当前麦位为 `pending_publish` 时确认成 `publishing` |
| `EventGroupId=2, EventType=204` | `audio_stopped` | 释放当前用户麦位 |
| `EventGroupId=1, EventType=104` | `room_exited` | 移除业务 presence,并释放麦位 |
| `EventGroupId=1, EventType=104` | `room_exited` | 释放当前用户麦位,不移除业务 presence |
回调边界:
@ -216,7 +216,8 @@ Header: Sign: base64(hmacsha256(callback_sign_key, raw_body))
- 房间状态变更只调用 room-service `ApplyRTCEvent`,仍进入 Room Cell、command log 和 outbox。
- 未知事件在验签成功后返回 `{"code":0}` 并忽略,避免腾讯新增事件导致无意义重试。
- room-service 暂时不可用时返回非 200让腾讯 RTC 按平台策略重试。
- 如果一个旧 `room_exited` 回调晚于新的业务心跳到达room-service 会保留较新的业务 presence避免旧回调清掉重连用户。
- `room_exited` 只代表 RTC 媒体频道退出,用户仍可能停留在房间页重连;业务 presence 只能由 `LeaveRoom`、踢人、关房或 stale worker 清理。
- 如果一个旧 `room_exited` 回调晚于新的业务心跳到达room-service 会保留较新的业务 presence 和新麦位会话,避免旧回调清掉重连用户。
## Internal Flow

View File

@ -10,6 +10,7 @@ type resourceDTO struct {
Name string `json:"name"`
Status string `json:"status"`
Grantable bool `json:"grantable"`
ManagerGrantEnabled bool `json:"managerGrantEnabled"`
GrantStrategy string `json:"grantStrategy"`
WalletAssetType string `json:"walletAssetType"`
WalletAssetAmount int64 `json:"walletAssetAmount"`
@ -120,6 +121,7 @@ func resourceFromProto(item *walletv1.Resource) resourceDTO {
Name: item.GetName(),
Status: item.GetStatus(),
Grantable: item.GetGrantable(),
ManagerGrantEnabled: item.GetManagerGrantEnabled(),
GrantStrategy: item.GetGrantStrategy(),
WalletAssetType: item.GetWalletAssetType(),
WalletAssetAmount: item.GetWalletAssetAmount(),

View File

@ -18,6 +18,7 @@ type resourceRequest struct {
Name string `json:"name"`
Status string `json:"status"`
Amount int64 `json:"amount"`
ManagerGrantEnabled *bool `json:"managerGrantEnabled"`
AssetURL string `json:"assetUrl"`
PreviewURL string `json:"previewUrl"`
SortOrder int32 `json:"sortOrder"`
@ -88,6 +89,7 @@ func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceReq
Name: strings.TrimSpace(r.Name),
Status: strings.TrimSpace(r.Status),
Grantable: true,
ManagerGrantEnabled: managerGrantEnabledOrDefault(r.ManagerGrantEnabled),
GrantStrategy: resourceGrantStrategy(resourceType),
WalletAssetType: walletAssetType,
WalletAssetAmount: walletAssetAmount,
@ -113,6 +115,7 @@ func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1
Name: strings.TrimSpace(r.Name),
Status: strings.TrimSpace(r.Status),
Grantable: true,
ManagerGrantEnabled: managerGrantEnabledOrDefault(r.ManagerGrantEnabled),
GrantStrategy: resourceGrantStrategy(resourceType),
WalletAssetType: walletAssetType,
WalletAssetAmount: walletAssetAmount,
@ -205,6 +208,14 @@ func (r giftRequest) updateProto(c *gin.Context, giftID string) *walletv1.Update
}
}
func managerGrantEnabledOrDefault(value *bool) *bool {
enabled := true
if value != nil {
enabled = *value
}
return &enabled
}
func groupItemInputs(items []resourceGroupItemRequest) []*walletv1.ResourceGroupItemInput {
out := make([]*walletv1.ResourceGroupItemInput, 0, len(items))
for _, item := range items {

View File

@ -10,6 +10,7 @@ import (
// RoomClient 抽象 gateway 对 room-service 的 gRPC 编排依赖。
type RoomClient interface {
CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest) (*roomv1.CreateRoomResponse, error)
UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (*roomv1.UpdateRoomProfileResponse, error)
JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error)
RoomHeartbeat(ctx context.Context, req *roomv1.RoomHeartbeatRequest) (*roomv1.RoomHeartbeatResponse, error)
LeaveRoom(ctx context.Context, req *roomv1.LeaveRoomRequest) (*roomv1.LeaveRoomResponse, error)
@ -81,6 +82,10 @@ func (c *grpcRoomClient) CreateRoom(ctx context.Context, req *roomv1.CreateRoomR
return c.client.CreateRoom(ctx, req)
}
func (c *grpcRoomClient) UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (*roomv1.UpdateRoomProfileResponse, error) {
return c.client.UpdateRoomProfile(ctx, req)
}
func (c *grpcRoomClient) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
return c.client.JoinRoom(ctx, req)
}

View File

@ -29,6 +29,7 @@ type UserIdentityClient interface {
// UserProfileClient 抽象 gateway 对 user-service 用户资料能力的依赖。
type UserProfileClient interface {
GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error)
BusinessUserLookup(ctx context.Context, req *userv1.BusinessUserLookupRequest) (*userv1.BusinessUserLookupResponse, error)
GetMyProfileStats(ctx context.Context, req *userv1.GetMyProfileStatsRequest) (*userv1.GetMyProfileStatsResponse, error)
BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error)
CompleteOnboarding(ctx context.Context, req *userv1.CompleteOnboardingRequest) (*userv1.CompleteOnboardingResponse, error)
@ -67,6 +68,7 @@ type UserHostClient interface {
GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, error)
GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error)
GetUserRoleSummary(ctx context.Context, req *userv1.GetUserRoleSummaryRequest) (*userv1.GetUserRoleSummaryResponse, error)
CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error)
}
// AppRegistryClient 抽象 gateway 对 user-service App 注册表的解析依赖。
@ -206,6 +208,10 @@ func (c *grpcUserProfileClient) GetUser(ctx context.Context, req *userv1.GetUser
return c.client.GetUser(ctx, req)
}
func (c *grpcUserProfileClient) BusinessUserLookup(ctx context.Context, req *userv1.BusinessUserLookupRequest) (*userv1.BusinessUserLookupResponse, error) {
return c.client.BusinessUserLookup(ctx, req)
}
func (c *grpcUserProfileClient) GetMyProfileStats(ctx context.Context, req *userv1.GetMyProfileStatsRequest) (*userv1.GetMyProfileStatsResponse, error) {
return c.client.GetMyProfileStats(ctx, req)
}
@ -294,6 +300,10 @@ func (c *grpcUserHostClient) GetUserRoleSummary(ctx context.Context, req *userv1
return c.client.GetUserRoleSummary(ctx, req)
}
func (c *grpcUserHostClient) CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error) {
return c.client.CheckBusinessCapability(ctx, req)
}
func (c *grpcAppRegistryClient) ResolveApp(ctx context.Context, req *userv1.ResolveAppRequest) (*userv1.ResolveAppResponse, error) {
return c.client.ResolveApp(ctx, req)
}

View File

@ -24,6 +24,7 @@ type WalletClient interface {
ListResources(ctx context.Context, req *walletv1.ListResourcesRequest) (*walletv1.ListResourcesResponse, error)
GetResourceGroup(ctx context.Context, req *walletv1.GetResourceGroupRequest) (*walletv1.GetResourceGroupResponse, error)
ListGiftConfigs(ctx context.Context, req *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error)
GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error)
ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error)
EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error)
}
@ -93,6 +94,10 @@ func (c *grpcWalletClient) ListGiftConfigs(ctx context.Context, req *walletv1.Li
return c.client.ListGiftConfigs(ctx, req)
}
func (c *grpcWalletClient) GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error) {
return c.client.GrantResource(ctx, req)
}
func (c *grpcWalletClient) ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) {
return c.client.ListUserResources(ctx, req)
}

View File

@ -385,8 +385,8 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
body.RoomName = strings.TrimSpace(body.RoomName)
body.RoomAvatar = strings.TrimSpace(body.RoomAvatar)
body.RoomDescription = strings.TrimSpace(body.RoomDescription)
if body.SeatCount <= 0 || body.Mode == "" || body.RoomName == "" {
// gateway mirrors room-service's hard requirements so client errors stay INVALID_ARGUMENT.
if body.SeatCount < 0 || body.Mode == "" || body.RoomName == "" {
// seat_count 可以省略走后台默认值mode 和 room_name 仍是创建页的最小必填信息。
writeError(writer, request, http.StatusBadRequest, codeInvalidArgument, "invalid argument")
return
}
@ -419,6 +419,47 @@ func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request)
write(writer, request, resp, err)
}
// updateRoomProfile 把 App 房间资料编辑请求转换为 room-service UpdateRoomProfile 命令。
func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
RoomName *string `json:"room_name"`
RoomAvatar *string `json:"room_avatar"`
RoomDescription *string `json:"room_description"`
SeatCount *int32 `json:"seat_count"`
}
if !decode(writer, request, &body) {
return
}
body.RoomID = strings.TrimSpace(body.RoomID)
body.CommandID = strings.TrimSpace(body.CommandID)
trimOptionalString(body.RoomName)
trimOptionalString(body.RoomAvatar)
trimOptionalString(body.RoomDescription)
resp, err := h.roomClient.UpdateRoomProfile(request.Context(), &roomv1.UpdateRoomProfileRequest{
Meta: meta(request, body.RoomID, body.CommandID),
RoomName: body.RoomName,
RoomAvatar: body.RoomAvatar,
RoomDescription: body.RoomDescription,
SeatCount: body.SeatCount,
})
if err != nil {
writeRPCError(writer, request, err)
return
}
writeOK(writer, request, updateRoomProfileDataFromProto(resp))
}
func trimOptionalString(value *string) {
if value == nil {
return
}
*value = strings.TrimSpace(*value)
}
// joinRoom 把进房 HTTP JSON 请求转换为 room-service JoinRoom 命令,并组装房间首屏初始化包。
func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
var body struct {

View File

@ -51,6 +51,13 @@ type joinRoomData struct {
ServerTimeMS int64 `json:"server_time_ms"`
}
type updateRoomProfileData struct {
Result roomCommandResultData `json:"result"`
Room roomInitialData `json:"room"`
Seats []roomSeatData `json:"seats"`
ServerTimeMS int64 `json:"server_time_ms"`
}
type roomCommandResultData struct {
Applied bool `json:"applied"`
RoomVersion int64 `json:"room_version"`
@ -176,6 +183,19 @@ func commandResultDataFromProto(result *roomv1.CommandResult) roomCommandResultD
}
}
func updateRoomProfileDataFromProto(resp *roomv1.UpdateRoomProfileResponse) updateRoomProfileData {
if resp == nil {
return updateRoomProfileData{}
}
snapshot := resp.GetRoom()
return updateRoomProfileData{
Result: commandResultDataFromProto(resp.GetResult()),
Room: roomInitialRoomDataFromSnapshot(snapshot, snapshot.GetRoomId()),
Seats: roomSeatDataFromSnapshot(snapshot),
ServerTimeMS: resp.GetResult().GetServerTimeMs(),
}
}
func roomInitialRoomDataFromSnapshot(snapshot *roomv1.RoomSnapshot, roomID string) roomInitialData {
if snapshot == nil {
return roomInitialData{RoomID: roomID, IMGroupID: roomIMGroupID(roomID)}

View File

@ -59,6 +59,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
mux.Handle(apiV1Prefix+"/rooms/current", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getCurrentRoom)))
mux.Handle(apiV1Prefix+"/rooms/snapshot", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodGet, h.getRoomSnapshot)))
mux.Handle(apiV1Prefix+"/rooms/create", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.createRoom)))
mux.Handle(apiV1Prefix+"/rooms/profile/update", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.updateRoomProfile)))
mux.Handle(apiV1Prefix+"/rooms/join", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.joinRoom)))
mux.Handle(apiV1Prefix+"/rooms/heartbeat", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.roomHeartbeat)))
mux.Handle(apiV1Prefix+"/rooms/leave", h.profileAPIHandler(jwtVerifier, requireMethod(http.MethodPost, h.leaveRoom)))

View File

@ -120,3 +120,26 @@ CREATE TABLE IF NOT EXISTS room_user_feed_entries (
PRIMARY KEY (app_code, user_id, feed_type, room_id),
KEY idx_room_user_feed_list (app_code, user_id, feed_type, updated_at_ms DESC, room_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS room_seat_configs (
app_code VARCHAR(32) NOT NULL,
allowed_seat_counts VARCHAR(128) NOT NULL,
default_seat_count INT NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO room_seat_configs (
app_code,
allowed_seat_counts,
default_seat_count,
created_at_ms,
updated_at_ms
) VALUES (
'lalu',
'[10,15,20,25,30]',
15,
0,
0
);

View File

@ -153,6 +153,20 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
base.SeatNo = body.GetSeatNo()
base.Attributes = map[string]string{"locked": fmt.Sprintf("%t", body.GetLocked())}
return base, true, nil
case "RoomProfileUpdated":
// 房间资料变更会影响标题、头像、简介和麦位布局,客户端收到后应以 room_version 更新本地 UI。
var body roomeventsv1.RoomProfileUpdated
if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil {
return tencentim.RoomEvent{}, false, err
}
base.ActorUserID = body.GetActorUserId()
base.Attributes = map[string]string{
"room_name": body.GetRoomName(),
"room_avatar": body.GetRoomAvatar(),
"room_description": body.GetRoomDescription(),
"seat_count": fmt.Sprintf("%d", body.GetSeatCount()),
}
return base, true, nil
case "RoomChatEnabledChanged":
// 公屏开关事件提示客户端更新输入态,真正发言仍由 CheckSpeakPermission 拦截。
var body roomeventsv1.RoomChatEnabledChanged
@ -245,6 +259,8 @@ func eventTypeForClient(eventType string) string {
return "room_mic_changed"
case "RoomMicSeatLocked":
return "room_mic_seat_locked"
case "RoomProfileUpdated":
return "room_profile_updated"
case "RoomChatEnabledChanged":
return "room_chat_enabled_changed"
case "RoomAdminChanged":

View File

@ -79,6 +79,22 @@ type CreateRoom struct {
// Type 返回命令类型。
func (CreateRoom) Type() string { return "create_room" }
// UpdateRoomProfile 定义房主修改房间展示资料和麦位数量的请求。
type UpdateRoomProfile struct {
Base
// RoomName 非 nil 表示更新房间展示名称;空字符串由服务层校验拒绝。
RoomName *string `json:"room_name,omitempty"`
// RoomAvatar 非 nil 表示更新房间头像;空字符串会恢复为系统默认头像。
RoomAvatar *string `json:"room_avatar,omitempty"`
// RoomDescription 非 nil 表示更新房间简介,允许清空。
RoomDescription *string `json:"room_description,omitempty"`
// SeatCount 非 nil 表示调整麦位总数,值必须来自后台启用配置。
SeatCount *int32 `json:"seat_count,omitempty"`
}
// Type 返回命令类型。
func (UpdateRoomProfile) Type() string { return "update_room_profile" }
// JoinRoom 定义业务进房请求。
type JoinRoom struct {
Base
@ -338,6 +354,8 @@ func Deserialize(commandType string, payload []byte) (Command, error) {
switch commandType {
case CreateRoom{}.Type():
cmd = &CreateRoom{}
case UpdateRoomProfile{}.Type():
cmd = &UpdateRoomProfile{}
case JoinRoom{}.Type():
cmd = &JoinRoom{}
case RoomHeartbeat{}.Type():

View File

@ -60,6 +60,10 @@ func (s *Service) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest)
if err != nil {
return nil, err
}
seatCount, err := s.resolveRoomSeatCount(ctx, req.GetSeatCount())
if err != nil {
return nil, err
}
actorUserID := req.GetMeta().GetActorUserId()
if actorUserID == 0 {
// CreateRoom 的 owner/host 默认来自已鉴权调用方,不允许匿名内部命令造房间。
@ -74,7 +78,7 @@ func (s *Service) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest)
Base: baseFromMeta(req.GetMeta()),
OwnerUserID: actorUserID,
HostUserID: actorUserID,
SeatCount: req.GetSeatCount(),
SeatCount: seatCount,
Mode: mode,
VisibleRegionID: normalizeVisibleRegionID(req.GetVisibleRegionId()),
RoomName: profile.Name,
@ -83,11 +87,6 @@ func (s *Service) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest)
RoomShortID: normalizeRoomShortID(req.GetRoomShortId(), actorUserID),
}
if cmd.SeatCount <= 0 {
// 没有麦位的房间不符合语音房 v1 的状态模型。
return nil, xerr.New(xerr.InvalidArgument, "seat_count must be positive")
}
payload, seen, err := s.commandPayloadForIdempotency(ctx, cmd)
if err != nil {
return nil, err

View File

@ -14,3 +14,8 @@ func int64String(value int64) string {
// 用户 ID 等数值进入 attributes 时保持十进制字符串,和 IM identifier 映射一致。
return strconv.FormatInt(value, 10)
}
func int32String(value int32) string {
// 麦位编号和座位总数同样使用十进制字符串,避免客户端解析多种数字格式。
return strconv.FormatInt(int64(value), 10)
}

View File

@ -84,6 +84,7 @@ func (s *Service) mutateRoom(ctx context.Context, cmd command.Command, replayabl
},
OutboxRecords: outboxRecords,
RoomStatus: result.roomStatus,
RoomSeatCount: result.roomSeatCount,
}); err != nil {
return nil, err
}

View File

@ -0,0 +1,195 @@
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/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"
)
// UpdateRoomProfile 修改房间展示资料和麦位数量,所有状态变化仍由 Room Cell 串行提交。
func (s *Service) UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (*roomv1.UpdateRoomProfileResponse, error) {
ctx = contextFromMeta(ctx, req.GetMeta())
patch, err := s.normalizeUpdateRoomProfile(ctx, req)
if err != nil {
return nil, err
}
cmd := command.UpdateRoomProfile{
Base: baseFromMeta(req.GetMeta()),
RoomName: patch.Name,
RoomAvatar: patch.Avatar,
RoomDescription: patch.Description,
SeatCount: patch.SeatCount,
}
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
if err := requireActiveRoom(current); err != nil {
return mutationResult{}, nil, err
}
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
return mutationResult{}, nil, err
}
profileChanged := applyProfilePatch(current, cmd)
seatChanged, err := applySeatCountPatch(current, cmd.SeatCount)
if err != nil {
return mutationResult{}, nil, err
}
if !profileChanged && !seatChanged {
// 未提交任何字段变化时仍通过 command log 保护 command_id 幂等,但不广播系统消息。
return mutationResult{snapshot: current.ToProto()}, nil, nil
}
current.Version++
seatCount := int32(len(current.MicSeats))
profileEvent, err := outbox.Build(current.RoomID, "RoomProfileUpdated", current.Version, now, &roomeventsv1.RoomProfileUpdated{
ActorUserId: cmd.ActorUserID(),
RoomName: current.RoomExt[roomExtTitleKey],
RoomAvatar: current.RoomExt[roomExtCoverURLKey],
RoomDescription: current.RoomExt[roomExtDescriptionKey],
SeatCount: seatCount,
})
if err != nil {
return mutationResult{}, nil, err
}
result := mutationResult{
snapshot: current.ToProto(),
syncEvent: &tencentim.RoomEvent{
EventID: profileEvent.EventID,
RoomID: current.RoomID,
EventType: "room_profile_updated",
ActorUserID: cmd.ActorUserID(),
RoomVersion: current.Version,
Attributes: map[string]string{
"room_name": current.RoomExt[roomExtTitleKey],
"room_avatar": current.RoomExt[roomExtCoverURLKey],
"room_description": current.RoomExt[roomExtDescriptionKey],
"seat_count": int32String(seatCount),
},
},
}
if seatChanged {
result.roomSeatCount = &seatCount
}
return result, []outbox.Record{profileEvent}, nil
})
if err != nil {
return nil, err
}
return &roomv1.UpdateRoomProfileResponse{
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
Room: result.snapshot,
}, nil
}
type updateRoomProfilePatch struct {
Name *string
Avatar *string
Description *string
SeatCount *int32
}
func (s *Service) normalizeUpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (updateRoomProfilePatch, error) {
var patch updateRoomProfilePatch
if req == nil {
return patch, xerr.New(xerr.InvalidArgument, "request is required")
}
if req.RoomName != nil {
value := strings.TrimSpace(req.GetRoomName())
if value == "" {
return patch, xerr.New(xerr.InvalidArgument, "room_name is required")
}
if utf8.RuneCountInString(value) > maxRoomNameRunes {
return patch, xerr.New(xerr.InvalidArgument, "room_name is too long")
}
patch.Name = &value
}
if req.RoomAvatar != nil {
value := strings.TrimSpace(req.GetRoomAvatar())
if value == "" {
// 房主清空头像时恢复系统默认资源,避免列表卡片落空。
value = defaultRoomAvatar
}
if utf8.RuneCountInString(value) > maxRoomAvatarRunes {
return patch, xerr.New(xerr.InvalidArgument, "room_avatar is too long")
}
patch.Avatar = &value
}
if req.RoomDescription != nil {
value := strings.TrimSpace(req.GetRoomDescription())
if utf8.RuneCountInString(value) > maxRoomDescriptionRunes {
return patch, xerr.New(xerr.InvalidArgument, "room_description is too long")
}
patch.Description = &value
}
if req.SeatCount != nil {
value, err := s.validateRoomSeatCount(ctx, req.GetSeatCount())
if err != nil {
return patch, err
}
patch.SeatCount = &value
}
return patch, nil
}
func applyProfilePatch(current *state.RoomState, cmd command.UpdateRoomProfile) bool {
if current.RoomExt == nil {
current.RoomExt = make(map[string]string)
}
changed := false
if cmd.RoomName != nil && current.RoomExt[roomExtTitleKey] != *cmd.RoomName {
current.RoomExt[roomExtTitleKey] = *cmd.RoomName
changed = true
}
if cmd.RoomAvatar != nil && current.RoomExt[roomExtCoverURLKey] != *cmd.RoomAvatar {
current.RoomExt[roomExtCoverURLKey] = *cmd.RoomAvatar
changed = true
}
if cmd.RoomDescription != nil && current.RoomExt[roomExtDescriptionKey] != *cmd.RoomDescription {
current.RoomExt[roomExtDescriptionKey] = *cmd.RoomDescription
changed = true
}
return changed
}
func applySeatCountPatch(current *state.RoomState, seatCount *int32) (bool, error) {
if seatCount == nil {
return false, nil
}
target := *seatCount
currentCount := int32(len(current.MicSeats))
if target == currentCount {
return false, nil
}
if target > currentCount {
for seatNo := currentCount + 1; seatNo <= target; seatNo++ {
current.MicSeats = append(current.MicSeats, state.MicSeat{SeatNo: seatNo})
}
return true, nil
}
nextSeats := make([]state.MicSeat, 0, target)
for _, seat := range current.MicSeats {
if seat.SeatNo <= target {
nextSeats = append(nextSeats, seat)
continue
}
if seat.UserID != 0 || seat.Locked || seat.PublishState != "" || seat.MicSessionID != "" {
// 缩容不能隐式踢人、清理发流会话或解锁麦位;房主需要先把高编号麦位处理干净。
return false, xerr.New(xerr.Conflict, "removed seats must be empty and unlocked")
}
}
current.MicSeats = nextSeats
return true, nil
}

View File

@ -126,6 +126,13 @@ func replay(current *state.RoomState, cmd command.Command) error {
})
// 创建命令的 owner/mode 等基础 meta 已由 RoomMeta 或快照恢复,这里只保证版本进入初始提交态。
current.Version = 1
case *command.UpdateRoomProfile:
// 房间资料和麦位总数都属于 Room Cell 状态;恢复时只重放已提交的确定性结果。
applyProfilePatch(current, *typed)
if _, err := applySeatCountPatch(current, typed.SeatCount); err != nil {
return err
}
current.Version++
case *command.JoinRoom:
if existing, exists := current.OnlineUsers[typed.ActorUserID()]; exists {
// 重复 JoinRoom 回放只刷新 last_seen不能重置首次进房时间或降级 owner 角色。
@ -214,7 +221,9 @@ func replay(current *state.RoomState, cmd command.Command) error {
current.ClearMicSession(index)
current.Version++
case rtcEventRoomExited:
delete(current.OnlineUsers, typed.TargetUserID)
// room_exited 是 RTC 媒体频道退出事实,不是 App 业务离房事实。
// 实时路径只释放麦位,业务 presence 只能由 LeaveRoom、KickUser、CloseRoom 或 stale worker 清理;
// replay 必须保持相同语义,否则无最新 snapshot 的恢复会把仍在房间页的用户误删。
if seat, exists := current.SeatByUser(typed.TargetUserID); exists {
index := current.SeatIndex(seat.SeatNo)
current.ClearMicSession(index)

View File

@ -61,6 +61,8 @@ type MutationCommit struct {
OutboxRecords []outbox.Record
// RoomStatus 非空时同步更新 rooms 和 room_list_entries 的生命周期状态。
RoomStatus string
// RoomSeatCount 非 nil 时同步更新 rooms 元数据中的麦位总数。
RoomSeatCount *int32
}
// SnapshotRecord 对应 room_snapshots 中的一条最新快照。
@ -117,6 +119,18 @@ type RoomListEntry struct {
FeedSubjectUserID int64
}
// RoomSeatConfig 是后台房间配置里控制创建/修改房间座位数的低频配置。
type RoomSeatConfig struct {
// AppCode 是配置所属 App。
AppCode string
// AllowedSeatCounts 是当前 App 允许客户端使用的麦位总数。
AllowedSeatCounts []int32
// DefaultSeatCount 是 CreateRoom 未传 seat_count 时使用的默认麦位总数。
DefaultSeatCount int32
// UpdatedAtMS 是配置最后更新时间,主要用于后台展示和排障。
UpdatedAtMS int64
}
// RoomPresence 是当前用户可恢复房间的轻量读模型。
// 它从 RoomSnapshot 投影而来,只用于 App 启动/回前台探测,不替代 Room Cell 权威状态。
type RoomPresence struct {
@ -246,6 +260,10 @@ type Repository interface {
SaveCommand(ctx context.Context, record CommandRecord) error
// SaveMutation 在同一事务中提交命令日志、outbox 事件和可选房间生命周期状态。
SaveMutation(ctx context.Context, commit MutationCommit) error
// GetRoomSeatConfig 读取后台配置的房间座位数规则;不存在时 service 层使用内置默认值。
GetRoomSeatConfig(ctx context.Context) (RoomSeatConfig, bool, error)
// UpsertRoomSeatConfig 写入房间座位数配置,供本地验证和后台配置共享同一张表。
UpsertRoomSeatConfig(ctx context.Context, config RoomSeatConfig) error
// ListCommandsAfter 读取快照版本之后的命令,恢复时按版本顺序回放。
ListCommandsAfter(ctx context.Context, roomID string, roomVersion int64) ([]CommandRecord, error)
// SaveSnapshot 保存最新房间快照,允许覆盖较低版本号快照但不能倒退。

View File

@ -0,0 +1,110 @@
package service
import (
"context"
"fmt"
"sort"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
)
const defaultRoomSeatCount int32 = 15
var roomSeatCountCandidates = []int32{10, 15, 20, 25, 30}
func defaultRoomSeatConfig(appCode string) RoomSeatConfig {
allowed := append([]int32(nil), roomSeatCountCandidates...)
return RoomSeatConfig{
AppCode: appcode.Normalize(appCode),
AllowedSeatCounts: allowed,
DefaultSeatCount: defaultRoomSeatCount,
}
}
func (s *Service) resolveRoomSeatCount(ctx context.Context, requested int32) (int32, error) {
if requested < 0 {
// 负数没有业务语义,不能让它落到默认值分支掩盖客户端错误。
return 0, xerr.New(xerr.InvalidArgument, "seat_count is invalid")
}
config, err := s.currentRoomSeatConfig(ctx)
if err != nil {
return 0, err
}
if requested == 0 {
return config.DefaultSeatCount, nil
}
if !seatCountAllowed(config.AllowedSeatCounts, requested) {
return 0, xerr.New(xerr.InvalidArgument, fmt.Sprintf("seat_count must be one of %v", config.AllowedSeatCounts))
}
return requested, nil
}
func (s *Service) validateRoomSeatCount(ctx context.Context, requested int32) (int32, error) {
if requested <= 0 {
return 0, xerr.New(xerr.InvalidArgument, "seat_count is invalid")
}
config, err := s.currentRoomSeatConfig(ctx)
if err != nil {
return 0, err
}
if !seatCountAllowed(config.AllowedSeatCounts, requested) {
return 0, xerr.New(xerr.InvalidArgument, fmt.Sprintf("seat_count must be one of %v", config.AllowedSeatCounts))
}
return requested, nil
}
func (s *Service) currentRoomSeatConfig(ctx context.Context) (RoomSeatConfig, error) {
config, exists, err := s.repository.GetRoomSeatConfig(ctx)
if err != nil {
return RoomSeatConfig{}, err
}
if !exists {
// 本地新库会通过 initdb/Migrate 写默认行;该兜底保护测试或手动库缺行时仍能按产品默认创建房间。
return defaultRoomSeatConfig(appcode.FromContext(ctx)), nil
}
normalized, err := normalizeRoomSeatConfig(config)
if err != nil {
return RoomSeatConfig{}, xerr.New(xerr.Unavailable, "room seat config is invalid")
}
return normalized, nil
}
func normalizeRoomSeatConfig(config RoomSeatConfig) (RoomSeatConfig, error) {
seen := make(map[int32]bool, len(config.AllowedSeatCounts))
allowed := make([]int32, 0, len(config.AllowedSeatCounts))
for _, value := range config.AllowedSeatCounts {
if seen[value] {
continue
}
if !seatCountAllowed(roomSeatCountCandidates, value) {
return RoomSeatConfig{}, fmt.Errorf("seat count candidate is invalid: %d", value)
}
seen[value] = true
allowed = append(allowed, value)
}
sort.Slice(allowed, func(i int, j int) bool {
return allowed[i] < allowed[j]
})
if len(allowed) == 0 {
return RoomSeatConfig{}, fmt.Errorf("allowed seat counts are empty")
}
if !seatCountAllowed(allowed, config.DefaultSeatCount) {
return RoomSeatConfig{}, fmt.Errorf("default seat count is not allowed: %d", config.DefaultSeatCount)
}
config.AllowedSeatCounts = allowed
config.AppCode = appcode.Normalize(config.AppCode)
return config, nil
}
func seatCountAllowed(values []int32, target int32) bool {
for _, value := range values {
if value == target {
return true
}
}
return false
}

View File

@ -99,6 +99,8 @@ type mutationResult struct {
outboxRecords []outbox.Record
// roomStatus 非空时代表命令提交时需要同步更新 rooms 和列表投影生命周期状态。
roomStatus string
// roomSeatCount 非 nil 时代表命令提交时需要同步更新 rooms 元数据座位数。
roomSeatCount *int32
}
// New 初始化 room-service 领域服务。

View File

@ -1111,6 +1111,58 @@ func TestOldRTCRoomExitedDoesNotClearRejoinedUserOrNewMicSession(t *testing.T) {
}
}
func TestRTCRoomExitedRecoveryKeepsBusinessPresence(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
directory := router.NewMemoryDirectory()
usedClock := &mutableClock{now: time.UnixMilli(1_700_000_000_000)}
serviceA := newRoomServiceWithClock("node-a", 100, directory, repository, &fakeSyncPublisher{}, &fakeOutboxPublisher{}, usedClock, time.Minute)
roomID := "room-rtc-exit-recovery-presence"
if _, err := serviceA.CreateRoom(ctx, &roomv1.CreateRoomRequest{Meta: roomservice.NewRequestMeta(roomID, 1), SeatCount: 4, Mode: "voice", RoomName: "Test Room"}); err != nil {
t.Fatalf("CreateRoom failed: %v", err)
}
if _, err := serviceA.JoinRoom(ctx, &roomv1.JoinRoomRequest{Meta: roomservice.NewRequestMeta(roomID, 2)}); err != nil {
t.Fatalf("JoinRoom user2 failed: %v", err)
}
if _, err := serviceA.MicUp(ctx, &roomv1.MicUpRequest{Meta: roomservice.NewRequestMeta(roomID, 2), SeatNo: 1}); err != nil {
t.Fatalf("MicUp user2 failed: %v", err)
}
usedClock.now = usedClock.now.Add(time.Second)
exitResp, err := serviceA.ApplyRTCEvent(ctx, &roomv1.ApplyRTCEventRequest{
Meta: roomservice.NewRequestMeta(roomID, 2),
TargetUserId: 2,
EventType: "room_exited",
EventTimeMs: usedClock.now.UnixMilli(),
Reason: "rtc_room_exited:recovery",
Source: "tencent_rtc_callback",
})
if err != nil {
t.Fatalf("ApplyRTCEvent room_exited failed: %v", err)
}
if user := findRoomUser(exitResp.GetRoom(), 2); user == nil {
t.Fatalf("real-time room_exited must keep business presence")
}
if seat := findSeat(exitResp.GetRoom(), 1); seat == nil || seat.GetUserId() != 0 {
t.Fatalf("real-time room_exited should release mic seat: seat=%+v", seat)
}
// snapshotEveryN=100 保证 room_exited 后没有最新快照node-b 必须通过 command log replay 恢复同一语义。
directory.ForceExpire(roomRouteKeyForTest(roomID))
serviceB := newRoomServiceWithClock("node-b", 100, directory, repository, &fakeSyncPublisher{}, &fakeOutboxPublisher{}, usedClock, time.Minute)
recoveredResp, err := serviceB.JoinRoom(ctx, &roomv1.JoinRoomRequest{Meta: roomservice.NewRequestMeta(roomID, 3)})
if err != nil {
t.Fatalf("JoinRoom on recovered service failed: %v", err)
}
if user := findRoomUser(recoveredResp.GetRoom(), 2); user == nil {
t.Fatalf("replayed room_exited must keep business presence")
}
if seat := findSeat(recoveredResp.GetRoom(), 1); seat == nil || seat.GetUserId() != 0 || seat.GetMicSessionId() != "" {
t.Fatalf("replayed room_exited should keep released mic seat: seat=%+v", seat)
}
}
func TestMicPublishTimeoutMicDownsPendingSession(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)

View File

@ -4,6 +4,7 @@ package mysql
import (
"context"
"database/sql"
"encoding/json"
"errors"
"sort"
"strings"
@ -178,6 +179,16 @@ func (r *Repository) Migrate(ctx context.Context) error {
PRIMARY KEY (app_code, user_id, feed_type, room_id),
KEY idx_room_user_feed_list (app_code, user_id, feed_type, updated_at_ms DESC, room_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`CREATE TABLE IF NOT EXISTS room_seat_configs (
app_code VARCHAR(32) NOT NULL,
allowed_seat_counts VARCHAR(128) NOT NULL,
default_seat_count INT NOT NULL,
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
PRIMARY KEY (app_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
`INSERT IGNORE INTO room_seat_configs (app_code, allowed_seat_counts, default_seat_count, created_at_ms, updated_at_ms)
VALUES ('lalu', '[10,15,20,25,30]', 15, 0, 0)`,
`ALTER TABLE rooms MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_list_entries MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_user_presence MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
@ -185,6 +196,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
`ALTER TABLE room_command_log MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_outbox MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_user_feed_entries MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
`ALTER TABLE room_seat_configs MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
}
for _, statement := range statements {
@ -401,10 +413,87 @@ func (r *Repository) SaveMutation(ctx context.Context, commit roomservice.Mutati
return err
}
}
if commit.RoomSeatCount != nil {
if _, err := tx.ExecContext(ctx,
`UPDATE rooms
SET seat_count = ?, updated_at_ms = ?
WHERE app_code = ? AND room_id = ?`,
*commit.RoomSeatCount,
commit.Command.CreatedAtMS,
appCode,
commit.Command.RoomID,
); err != nil {
_ = tx.Rollback()
return err
}
if _, err := tx.ExecContext(ctx,
`UPDATE room_list_entries
SET seat_count = ?, updated_at_ms = ?
WHERE app_code = ? AND room_id = ?`,
*commit.RoomSeatCount,
commit.Command.CreatedAtMS,
appCode,
commit.Command.RoomID,
); err != nil {
_ = tx.Rollback()
return err
}
}
return tx.Commit()
}
// GetRoomSeatConfig 读取当前 App 的房间座位数配置。
func (r *Repository) GetRoomSeatConfig(ctx context.Context) (roomservice.RoomSeatConfig, bool, error) {
row := r.db.QueryRowContext(ctx,
`SELECT app_code, allowed_seat_counts, default_seat_count, updated_at_ms
FROM room_seat_configs
WHERE app_code = ?`,
appcode.FromContext(ctx),
)
var config roomservice.RoomSeatConfig
var rawAllowed string
if err := row.Scan(&config.AppCode, &rawAllowed, &config.DefaultSeatCount, &config.UpdatedAtMS); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return roomservice.RoomSeatConfig{}, false, nil
}
return roomservice.RoomSeatConfig{}, false, err
}
if err := json.Unmarshal([]byte(rawAllowed), &config.AllowedSeatCounts); err != nil {
return roomservice.RoomSeatConfig{}, false, err
}
return config, true, nil
}
// UpsertRoomSeatConfig 写入当前 App 的房间座位数配置。
func (r *Repository) UpsertRoomSeatConfig(ctx context.Context, config roomservice.RoomSeatConfig) error {
appCode := normalizedRecordAppCode(ctx, config.AppCode)
rawAllowed, err := json.Marshal(config.AllowedSeatCounts)
if err != nil {
return err
}
nowMS := config.UpdatedAtMS
if nowMS <= 0 {
nowMS = time.Now().UTC().UnixMilli()
}
_, err = r.db.ExecContext(ctx,
`INSERT INTO room_seat_configs (app_code, allowed_seat_counts, default_seat_count, created_at_ms, updated_at_ms)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
allowed_seat_counts = VALUES(allowed_seat_counts),
default_seat_count = VALUES(default_seat_count),
updated_at_ms = VALUES(updated_at_ms)`,
appCode,
string(rawAllowed),
config.DefaultSeatCount,
nowMS,
nowMS,
)
return err
}
// ListCommandsAfter 返回快照版本之后的可回放命令日志。
func (r *Repository) ListCommandsAfter(ctx context.Context, roomID string, roomVersion int64) ([]roomservice.CommandRecord, error) {
// 恢复必须按 room_version 再按自增 id 排序,确保同版本异常数据也有稳定顺序。

View File

@ -47,6 +47,13 @@ func (s *Server) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest)
return mapServiceResult(s.svc.CreateRoom(ctx, req))
}
// UpdateRoomProfile 代理到领域服务。
func (s *Server) UpdateRoomProfile(ctx context.Context, req *roomv1.UpdateRoomProfileRequest) (*roomv1.UpdateRoomProfileResponse, error) {
// 房间资料和座位数变更必须经 Room Cell 串行提交gRPC 层不直接修改快照。
ctx = contextWithMetaApp(ctx, req.GetMeta())
return mapServiceResult(s.svc.UpdateRoomProfile(ctx, req))
}
// JoinRoom 代理到领域服务。
func (s *Server) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) {
// JoinRoom 只维护 room-service presence真实消息连接由腾讯云 IM SDK 处理。

View File

@ -225,6 +225,19 @@ type FriendApplication struct {
UpdatedAtMs int64
}
// BusinessUserLookupItem 是带业务场景权限裁剪后的用户查询结果。
// 它只包含选择目标用户所需字段,不能扩展手机号、登录身份或钱包资产。
type BusinessUserLookupItem struct {
UserID int64
DisplayUserID string
Username string
Avatar string
Status Status
RegionID int64
RegionCode string
RegionName string
}
// Identity 是 user_id 与当前 active display_user_id 的绑定投影。
type Identity struct {
// AppCode 是短号所属 App。

View File

@ -0,0 +1,38 @@
package host
import (
"context"
"strings"
"hyapp/pkg/xerr"
)
const (
CapabilityManagerCenter = "manager_center"
CapabilityManagerResourceGrant = "manager_resource_grant"
)
// CheckBusinessCapability 是 App/H5 业务入口的服务端身份边界。
// 首版经理中心只认 active Agency owner不把客户端自报角色或普通 Agency 成员当作授权依据。
func (s *Service) CheckBusinessCapability(ctx context.Context, actorUserID int64, capability string) (bool, string, error) {
if s.repository == nil {
return false, "", xerr.New(xerr.Unavailable, "host repository is not configured")
}
if actorUserID <= 0 {
return false, "", xerr.New(xerr.InvalidArgument, "actor_user_id is required")
}
capability = strings.ToLower(strings.TrimSpace(capability))
switch capability {
case CapabilityManagerCenter, CapabilityManagerResourceGrant:
_, ok, err := s.repository.HasActiveAgencyOwner(ctx, actorUserID)
if err != nil {
return false, "", err
}
if !ok {
return false, "active_agency_owner_required", nil
}
return true, "", nil
default:
return false, "", xerr.New(xerr.InvalidArgument, "business capability is invalid")
}
}

View File

@ -33,6 +33,7 @@ type Repository interface {
GetBDProfile(ctx context.Context, userID int64) (hostdomain.BDProfile, error)
GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error)
GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error)
HasActiveAgencyOwner(ctx context.Context, userID int64) (int64, bool, error)
ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error)
ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error)
}

View File

@ -0,0 +1,53 @@
package user
import (
"context"
"strings"
"unicode/utf8"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
)
const (
BusinessSceneManagerResourceGrant = "manager_resource_grant"
defaultBusinessLookupPageSize = 20
maxBusinessLookupPageSize = 20
)
// BusinessUserLookup 执行业务场景用户查询。调用方必须先完成该 scene 的能力校验;
// service 层仍校验 scene 和 keyword避免未来入口把它误用成全局用户搜索。
func (s *Service) BusinessUserLookup(ctx context.Context, scene string, keyword string, pageSize int32) ([]userdomain.BusinessUserLookupItem, error) {
scene = strings.ToLower(strings.TrimSpace(scene))
if scene != BusinessSceneManagerResourceGrant {
return nil, xerr.New(xerr.InvalidArgument, "business lookup scene is invalid")
}
if s.userRepository == nil {
return nil, xerr.New(xerr.Unavailable, "user repository is not configured")
}
keyword = strings.TrimSpace(keyword)
numericKeyword := isASCIIInteger(keyword)
if keyword == "" || (!numericKeyword && utf8.RuneCountInString(keyword) < 2) {
return nil, xerr.New(xerr.InvalidArgument, "keyword is invalid")
}
if pageSize <= 0 {
pageSize = defaultBusinessLookupPageSize
}
if pageSize > maxBusinessLookupPageSize {
pageSize = maxBusinessLookupPageSize
}
return s.userRepository.BusinessUserLookup(ctx, keyword, numericKeyword, pageSize)
}
func isASCIIInteger(value string) bool {
if value == "" {
return false
}
for _, ch := range value {
if ch < '0' || ch > '9' {
return false
}
}
return true
}

View File

@ -36,6 +36,8 @@ type UserRepository interface {
ListFriends(ctx context.Context, userID int64, page int32, pageSize int32, cursorUpdatedAtMS int64, cursorUserID int64) ([]userdomain.FriendRecord, int64, error)
// ListFriendApplications 分页读取好友申请。
ListFriendApplications(ctx context.Context, userID int64, direction string, page int32, pageSize int32) ([]userdomain.FriendApplication, int64, error)
// BusinessUserLookup 按已授权业务场景查询目标用户,返回字段必须保持最小化。
BusinessUserLookup(ctx context.Context, keyword string, numericKeyword bool, pageSize int32) ([]userdomain.BusinessUserLookupItem, error)
// BatchGetUsers 批量查询用户主状态,缺失用户不应返回占位对象。
BatchGetUsers(ctx context.Context, userIDs []int64) (map[int64]userdomain.User, error)
// ListUserIDs 按稳定 user_id 游标读取低频后台任务目标用户。

View File

@ -147,6 +147,16 @@ func (r *Repository) GetUserRoleSummary(ctx context.Context, userID int64) (host
return summary, nil
}
// HasActiveAgencyOwner 精确判断用户是否为 active Agency owner。
// 普通 Agency 成员会在 UserRoleSummary 中有 Agency 入口,但不能获得经理中心资源赠送权限。
func (r *Repository) HasActiveAgencyOwner(ctx context.Context, userID int64) (int64, bool, error) {
agency, ok, err := queryActiveAgencyByOwner(ctx, r.db, userID, false)
if err != nil || !ok {
return 0, ok, err
}
return agency.AgencyID, true, nil
}
// ListAgencyMembers 读取 Agency 成员关系列表。
func (r *Repository) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) {
query := fmt.Sprintf(`SELECT %s FROM agency_memberships WHERE app_code = ? AND agency_id = ?`, agencyMembershipColumns)

View File

@ -0,0 +1,79 @@
package user
import (
"context"
"strconv"
"strings"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
)
// BusinessUserLookup 只返回同 app_code 的 active 用户,并按查询类型收敛匹配范围。
// 数字 keyword 只做短号和 user_id 精确匹配;昵称搜索要求调用方已校验最小长度。
func (r *Repository) BusinessUserLookup(ctx context.Context, keyword string, numericKeyword bool, pageSize int32) ([]userdomain.BusinessUserLookupItem, error) {
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
app := appcode.FromContext(ctx)
limit := int(pageSize)
if limit <= 0 {
limit = 20
}
var args []any
query := `SELECT ` + userSelectColumns + ` FROM users WHERE app_code = ? AND status = ?`
args = append(args, app, string(userdomain.StatusActive))
if numericKeyword {
userID := int64(0)
if parsed, err := strconv.ParseInt(keyword, 10, 64); err == nil {
userID = parsed
}
query += ` AND (current_display_user_id = ? OR user_id = ?)
ORDER BY CASE WHEN current_display_user_id = ? THEN 0 WHEN user_id = ? THEN 1 ELSE 2 END, updated_at_ms DESC, user_id DESC
LIMIT ?`
args = append(args, keyword, userID, keyword, userID, limit)
} else {
query += ` AND username LIKE ? ESCAPE '\\'
ORDER BY updated_at_ms DESC, user_id DESC
LIMIT ?`
args = append(args, "%"+escapeLikeKeyword(keyword)+"%", limit)
}
rows, err := r.db.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]userdomain.BusinessUserLookupItem, 0, limit)
for rows.Next() {
user, err := ScanUser(rows)
if err != nil {
return nil, err
}
items = append(items, businessUserLookupItem(user))
}
return items, rows.Err()
}
func businessUserLookupItem(user userdomain.User) userdomain.BusinessUserLookupItem {
return userdomain.BusinessUserLookupItem{
UserID: user.UserID,
DisplayUserID: user.CurrentDisplayUserID,
Username: user.Username,
Avatar: user.Avatar,
Status: user.Status,
RegionID: user.RegionID,
RegionCode: user.RegionCode,
RegionName: user.RegionName,
}
}
func escapeLikeKeyword(value string) string {
value = strings.ReplaceAll(value, `\`, `\\`)
value = strings.ReplaceAll(value, `%`, `\%`)
value = strings.ReplaceAll(value, `_`, `\_`)
return value
}

View File

@ -80,6 +80,19 @@ func toProtoUser(user userdomain.User) *userv1.User {
}
}
func toProtoBusinessUserLookupItem(item userdomain.BusinessUserLookupItem) *userv1.BusinessUserLookupItem {
return &userv1.BusinessUserLookupItem{
UserId: item.UserID,
DisplayUserId: item.DisplayUserID,
Username: item.Username,
Avatar: item.Avatar,
Status: toProtoStatus(item.Status),
RegionId: item.RegionID,
RegionCode: item.RegionCode,
RegionName: item.RegionName,
}
}
func toProtoInviteOverview(overview invitedomain.Overview) *userv1.InviteOverview {
if overview.MyInviteCode == "" && !overview.InviteEnabled && overview.InviteCount == 0 && overview.ValidInviteCount == 0 && overview.ValidInviteThresholdCoin == 0 {
return nil

View File

@ -220,6 +220,19 @@ func (s *Server) GetUserRoleSummary(ctx context.Context, req *userv1.GetUserRole
return &userv1.GetUserRoleSummaryResponse{Summary: toProtoUserRoleSummary(summary)}, nil
}
// CheckBusinessCapability 返回 H5/App 业务入口可用性,具体身份规则由 host service 统一判断。
func (s *Server) CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error) {
if s.hostSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
}
ctx = contextWithApp(ctx, req.GetMeta())
allowed, reason, err := s.hostSvc.CheckBusinessCapability(ctx, req.GetActorUserId(), req.GetCapability())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.CheckBusinessCapabilityResponse{Allowed: allowed, Reason: reason}, nil
}
// GetAgencyMembers 返回 Agency 成员关系事实列表。
func (s *Server) GetAgencyMembers(ctx context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error) {
if s.hostSvc == nil {

View File

@ -3,6 +3,7 @@ package grpc
import (
"context"
"strings"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/pkg/appcode"
@ -178,6 +179,46 @@ func (s *Server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*user
return &userv1.GetUserResponse{User: toProtoUser(user)}, nil
}
// BusinessUserLookup 是带业务场景和能力校验的用户查询入口。
func (s *Server) BusinessUserLookup(ctx context.Context, req *userv1.BusinessUserLookupRequest) (*userv1.BusinessUserLookupResponse, error) {
if s.hostSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
}
ctx = contextWithApp(ctx, req.GetMeta())
capability, ok := capabilityForBusinessLookupScene(req.GetScene())
if !ok {
return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "business lookup scene is invalid"))
}
allowed, reason, err := s.hostSvc.CheckBusinessCapability(ctx, req.GetActorUserId(), capability)
if err != nil {
return nil, xerr.ToGRPCError(err)
}
if !allowed {
if reason == "" {
reason = "permission denied"
}
return nil, xerr.ToGRPCError(xerr.New(xerr.PermissionDenied, reason))
}
users, err := s.userSvc.BusinessUserLookup(ctx, req.GetScene(), req.GetKeyword(), req.GetPageSize())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &userv1.BusinessUserLookupResponse{Users: make([]*userv1.BusinessUserLookupItem, 0, len(users))}
for _, user := range users {
resp.Users = append(resp.Users, toProtoBusinessUserLookupItem(user))
}
return resp, nil
}
func capabilityForBusinessLookupScene(scene string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(scene)) {
case userservice.BusinessSceneManagerResourceGrant:
return hostservice.CapabilityManagerResourceGrant, true
default:
return "", false
}
}
// GetMyProfileStats 返回我的页顶部统计计数。
func (s *Server) GetMyProfileStats(ctx context.Context, req *userv1.GetMyProfileStatsRequest) (*userv1.GetMyProfileStatsResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())

View File

@ -160,6 +160,7 @@ CREATE TABLE IF NOT EXISTS resources (
name VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
grantable BOOLEAN NOT NULL DEFAULT TRUE,
manager_grant_enabled BOOLEAN NOT NULL DEFAULT TRUE,
grant_strategy VARCHAR(32) NOT NULL,
wallet_asset_type VARCHAR(32) NOT NULL DEFAULT '',
wallet_asset_amount BIGINT NOT NULL DEFAULT 0,
@ -174,9 +175,28 @@ CREATE TABLE IF NOT EXISTS resources (
created_at_ms BIGINT NOT NULL,
updated_at_ms BIGINT NOT NULL,
UNIQUE KEY uk_resources_code (app_code, resource_code),
KEY idx_resources_type_status (app_code, resource_type, status, sort_order)
KEY idx_resources_type_status (app_code, resource_type, status, sort_order),
KEY idx_resources_manager_grant (app_code, manager_grant_enabled, status, grantable, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND COLUMN_NAME = 'manager_grant_enabled') = 0,
'ALTER TABLE resources ADD COLUMN manager_grant_enabled BOOLEAN NOT NULL DEFAULT TRUE AFTER grantable',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND INDEX_NAME = 'idx_resources_manager_grant') = 0,
'ALTER TABLE resources ADD KEY idx_resources_manager_grant (app_code, manager_grant_enabled, status, grantable, sort_order)',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
CREATE TABLE IF NOT EXISTS resource_groups (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu',
group_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,

View File

@ -27,10 +27,11 @@ const (
GrantSubjectResource = "resource"
GrantSubjectGroup = "resource_group"
GrantSourceAdmin = "admin"
GrantStatusDone = "succeeded"
ResultWalletCredit = "wallet_credit"
ResultEntitlement = "entitlement"
GrantSourceAdmin = "admin"
GrantSourceManagerCenter = "manager_center"
GrantStatusDone = "succeeded"
ResultWalletCredit = "wallet_credit"
ResultEntitlement = "entitlement"
GroupItemTypeResource = "resource"
GroupItemTypeWalletAsset = "wallet_asset"
@ -72,6 +73,7 @@ type Resource struct {
UpdatedByUserID int64
CreatedAtMS int64
UpdatedAtMS int64
ManagerGrantEnabled bool
}
type ResourceGroupItem struct {
@ -182,6 +184,7 @@ type ListResourcesQuery struct {
Page int32
PageSize int32
ActiveOnly bool
ManagerGrantOnly bool
}
type ResourceCommand struct {
@ -202,6 +205,7 @@ type ResourceCommand struct {
MetadataJSON string
SortOrder int32
OperatorUserID int64
ManagerGrantEnabled bool
}
type StatusCommand struct {

View File

@ -48,14 +48,7 @@ func (r *Repository) ListResources(ctx context.Context, query resourcedomain.Lis
if err != nil {
return nil, 0, err
}
rows, err := r.db.QueryContext(ctx, `
SELECT app_code, resource_id, resource_code, resource_type, name, status, grantable,
grant_strategy, wallet_asset_type, wallet_asset_amount,
COALESCE(CAST(usage_scope_json AS CHAR), '[]'),
asset_url, preview_url, animation_url,
COALESCE(CAST(metadata_json AS CHAR), '{}'),
sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
FROM resources
rows, err := r.db.QueryContext(ctx, resourceSelectSQL()+`
`+where+`
ORDER BY sort_order ASC, resource_id DESC
LIMIT ? OFFSET ?`,
@ -118,17 +111,18 @@ func (r *Repository) CreateResource(ctx context.Context, command resourcedomain.
result, err := tx.ExecContext(ctx, `
INSERT INTO resources (
app_code, resource_code, resource_type, name, status, grantable, grant_strategy,
app_code, resource_code, resource_type, name, status, grantable, manager_grant_enabled, grant_strategy,
wallet_asset_type, wallet_asset_amount, usage_scope_json, asset_url, preview_url,
animation_url, metadata_json, sort_order, created_by_user_id, updated_by_user_id,
created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
command.AppCode,
command.ResourceCode,
command.ResourceType,
command.Name,
command.Status,
command.Grantable,
command.ManagerGrantEnabled,
command.GrantStrategy,
command.WalletAssetType,
command.WalletAssetAmount,
@ -193,7 +187,7 @@ func (r *Repository) UpdateResource(ctx context.Context, command resourcedomain.
result, err := tx.ExecContext(ctx, `
UPDATE resources
SET resource_code = ?, resource_type = ?, name = ?, status = ?, grantable = ?,
SET resource_code = ?, resource_type = ?, name = ?, status = ?, grantable = ?, manager_grant_enabled = ?,
grant_strategy = ?, wallet_asset_type = ?, wallet_asset_amount = ?,
usage_scope_json = ?, asset_url = ?, preview_url = ?, animation_url = ?,
metadata_json = ?, sort_order = ?, updated_by_user_id = ?, updated_at_ms = ?
@ -203,6 +197,7 @@ func (r *Repository) UpdateResource(ctx context.Context, command resourcedomain.
command.Name,
command.Status,
command.Grantable,
command.ManagerGrantEnabled,
command.GrantStrategy,
command.WalletAssetType,
command.WalletAssetAmount,
@ -624,7 +619,7 @@ func (r *Repository) GrantResource(ctx context.Context, command resourcedomain.G
if err != nil {
return resourcedomain.ResourceGrant{}, err
}
if err := validateGrantableResource(resource); err != nil {
if err := validateGrantableResourceForSource(resource, command.GrantSource); err != nil {
return resourcedomain.ResourceGrant{}, err
}
grantID := resourceGrantID(command.AppCode, command.CommandID)
@ -1638,6 +1633,7 @@ func scanResource(scanner scanTarget) (resourcedomain.Resource, error) {
&resource.Name,
&resource.Status,
&resource.Grantable,
&resource.ManagerGrantEnabled,
&resource.GrantStrategy,
&resource.WalletAssetType,
&resource.WalletAssetAmount,
@ -1694,6 +1690,7 @@ func scanResourceGroupItem(scanner scanTarget) (resourcedomain.ResourceGroupItem
&resource.Name,
&resource.Status,
&resource.Grantable,
&resource.ManagerGrantEnabled,
&resource.GrantStrategy,
&resource.WalletAssetType,
&resource.WalletAssetAmount,
@ -1748,6 +1745,7 @@ func scanGiftConfig(scanner scanTarget) (resourcedomain.GiftConfig, error) {
&resource.Name,
&resource.Status,
&resource.Grantable,
&resource.ManagerGrantEnabled,
&resource.GrantStrategy,
&resource.WalletAssetType,
&resource.WalletAssetAmount,
@ -1794,6 +1792,7 @@ func scanUserResourceEntitlement(scanner scanTarget) (resourcedomain.UserResourc
&resource.Name,
&resource.Status,
&resource.Grantable,
&resource.ManagerGrantEnabled,
&resource.GrantStrategy,
&resource.WalletAssetType,
&resource.WalletAssetAmount,
@ -1844,7 +1843,7 @@ func resourceColumnsWithAlias(alias string) string {
prefix = alias + "."
}
return prefix + `app_code, ` + prefix + `resource_id, ` + prefix + `resource_code, ` + prefix + `resource_type, ` +
prefix + `name, ` + prefix + `status, ` + prefix + `grantable, ` + prefix + `grant_strategy, ` +
prefix + `name, ` + prefix + `status, ` + prefix + `grantable, ` + prefix + `manager_grant_enabled, ` + prefix + `grant_strategy, ` +
prefix + `wallet_asset_type, ` + prefix + `wallet_asset_amount, COALESCE(CAST(` + prefix + `usage_scope_json AS CHAR), '[]'), ` +
prefix + `asset_url, ` + prefix + `preview_url, ` + prefix + `animation_url, COALESCE(CAST(` + prefix + `metadata_json AS CHAR), '{}'), ` +
prefix + `sort_order, ` + prefix + `created_by_user_id, ` + prefix + `updated_by_user_id, ` + prefix + `created_at_ms, ` + prefix + `updated_at_ms`
@ -1857,7 +1856,7 @@ func nullableResourceColumnsWithAlias(alias string) string {
}
return `COALESCE(` + prefix + `app_code, ''), COALESCE(` + prefix + `resource_id, 0), COALESCE(` + prefix + `resource_code, ''), ` +
`COALESCE(` + prefix + `resource_type, ''), COALESCE(` + prefix + `name, ''), COALESCE(` + prefix + `status, ''), ` +
`COALESCE(` + prefix + `grantable, FALSE), COALESCE(` + prefix + `grant_strategy, ''), COALESCE(` + prefix + `wallet_asset_type, ''), ` +
`COALESCE(` + prefix + `grantable, FALSE), COALESCE(` + prefix + `manager_grant_enabled, FALSE), COALESCE(` + prefix + `grant_strategy, ''), COALESCE(` + prefix + `wallet_asset_type, ''), ` +
`COALESCE(` + prefix + `wallet_asset_amount, 0), COALESCE(CAST(` + prefix + `usage_scope_json AS CHAR), '[]'), ` +
`COALESCE(` + prefix + `asset_url, ''), COALESCE(` + prefix + `preview_url, ''), COALESCE(` + prefix + `animation_url, ''), ` +
`COALESCE(CAST(` + prefix + `metadata_json AS CHAR), '{}'), COALESCE(` + prefix + `sort_order, 0), ` +
@ -1900,7 +1899,9 @@ func resourceGrantSelectSQL() string {
func resourceWhereSQL(query resourcedomain.ListResourcesQuery) (string, []any) {
where := `WHERE app_code = ?`
args := []any{query.AppCode}
if query.ActiveOnly {
if query.ManagerGrantOnly {
where += ` AND status = 'active' AND grantable = TRUE AND manager_grant_enabled = TRUE`
} else if query.ActiveOnly {
where += ` AND status = 'active'`
} else if query.Status != "" {
where += ` AND status = ?`
@ -2218,6 +2219,19 @@ func validateGrantResourceGroupCommand(command resourcedomain.GrantResourceGroup
if command.CommandID == "" || command.TargetUserID <= 0 || command.GroupID <= 0 || command.OperatorUserID <= 0 || command.Reason == "" {
return xerr.New(xerr.InvalidArgument, "resource group grant command is incomplete")
}
if command.GrantSource == resourcedomain.GrantSourceManagerCenter {
return xerr.New(xerr.InvalidArgument, "manager center cannot grant resource group")
}
return nil
}
func validateGrantableResourceForSource(resource resourcedomain.Resource, grantSource string) error {
if err := validateGrantableResource(resource); err != nil {
return err
}
if grantSource == resourcedomain.GrantSourceManagerCenter && !resource.ManagerGrantEnabled {
return xerr.New(xerr.PermissionDenied, "resource is not enabled for manager center grant")
}
return nil
}

View File

@ -11,13 +11,14 @@ import (
func (s *Server) ListResources(ctx context.Context, req *walletv1.ListResourcesRequest) (*walletv1.ListResourcesResponse, error) {
items, total, err := s.svc.ListResources(ctx, resourcedomain.ListResourcesQuery{
AppCode: req.GetAppCode(),
ResourceType: req.GetResourceType(),
Status: req.GetStatus(),
Keyword: req.GetKeyword(),
Page: req.GetPage(),
PageSize: req.GetPageSize(),
ActiveOnly: req.GetActiveOnly(),
AppCode: req.GetAppCode(),
ResourceType: req.GetResourceType(),
Status: req.GetStatus(),
Keyword: req.GetKeyword(),
Page: req.GetPage(),
PageSize: req.GetPageSize(),
ActiveOnly: req.GetActiveOnly(),
ManagerGrantOnly: req.GetManagerGrantOnly(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
@ -270,8 +271,9 @@ func resourceCommandFromCreate(req *walletv1.CreateResourceRequest) resourcedoma
PreviewURL: req.GetPreviewUrl(),
AnimationURL: req.GetAnimationUrl(),
MetadataJSON: req.GetMetadataJson(),
SortOrder: req.GetSortOrder(),
OperatorUserID: req.GetOperatorUserId(),
SortOrder: req.GetSortOrder(),
OperatorUserID: req.GetOperatorUserId(),
ManagerGrantEnabled: managerGrantEnabledOrDefault(req.ManagerGrantEnabled),
}
}
@ -291,8 +293,9 @@ func resourceCommandFromUpdate(req *walletv1.UpdateResourceRequest) resourcedoma
PreviewUrl: req.GetPreviewUrl(),
AnimationUrl: req.GetAnimationUrl(),
MetadataJson: req.GetMetadataJson(),
SortOrder: req.GetSortOrder(),
OperatorUserId: req.GetOperatorUserId(),
SortOrder: req.GetSortOrder(),
OperatorUserId: req.GetOperatorUserId(),
ManagerGrantEnabled: managerGrantEnabledOrDefault(req.ManagerGrantEnabled),
})
command.ResourceID = req.GetResourceId()
return command
@ -412,11 +415,20 @@ func resourceToProto(resource resourcedomain.Resource) *walletv1.Resource {
SortOrder: resource.SortOrder,
CreatedByUserId: resource.CreatedByUserID,
UpdatedByUserId: resource.UpdatedByUserID,
CreatedAtMs: resource.CreatedAtMS,
UpdatedAtMs: resource.UpdatedAtMS,
CreatedAtMs: resource.CreatedAtMS,
UpdatedAtMs: resource.UpdatedAtMS,
ManagerGrantEnabled: resource.ManagerGrantEnabled,
}
}
func managerGrantEnabledOrDefault(value *bool) bool {
if value == nil {
// 新建/编辑入口如果没显式携带开关,仍按产品规则默认允许经理中心赠送。
return true
}
return *value
}
func resourceGroupToProto(group resourcedomain.ResourceGroup) *walletv1.ResourceGroup {
items := make([]*walletv1.ResourceGroupItem, 0, len(group.Items))
for _, item := range group.Items {