修复多人送礼物问题

This commit is contained in:
zhx 2026-06-03 18:19:50 +08:00
parent 10a3df3a5a
commit 3c7648f98e
51 changed files with 6395 additions and 1704 deletions

View File

@ -348,19 +348,128 @@ func (x *RoomBackgroundChanged) GetRoomBackgroundUrl() string {
}
// RoomUserJoined 表达用户业务态进房成功。
type RoomEntryVehicleSnapshot struct {
state protoimpl.MessageState `protogen:"open.v1"`
ResourceId int64 `protobuf:"varint,1,opt,name=resource_id,json=resourceId,proto3" json:"resource_id,omitempty"`
ResourceCode string `protobuf:"bytes,2,opt,name=resource_code,json=resourceCode,proto3" json:"resource_code,omitempty"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"`
AssetUrl string `protobuf:"bytes,4,opt,name=asset_url,json=assetUrl,proto3" json:"asset_url,omitempty"`
PreviewUrl string `protobuf:"bytes,5,opt,name=preview_url,json=previewUrl,proto3" json:"preview_url,omitempty"`
AnimationUrl string `protobuf:"bytes,6,opt,name=animation_url,json=animationUrl,proto3" json:"animation_url,omitempty"`
MetadataJson string `protobuf:"bytes,7,opt,name=metadata_json,json=metadataJson,proto3" json:"metadata_json,omitempty"`
EntitlementId string `protobuf:"bytes,8,opt,name=entitlement_id,json=entitlementId,proto3" json:"entitlement_id,omitempty"`
ExpiresAtMs int64 `protobuf:"varint,9,opt,name=expires_at_ms,json=expiresAtMs,proto3" json:"expires_at_ms,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomEntryVehicleSnapshot) Reset() {
*x = RoomEntryVehicleSnapshot{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RoomEntryVehicleSnapshot) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RoomEntryVehicleSnapshot) ProtoMessage() {}
func (x *RoomEntryVehicleSnapshot) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RoomEntryVehicleSnapshot.ProtoReflect.Descriptor instead.
func (*RoomEntryVehicleSnapshot) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{4}
}
func (x *RoomEntryVehicleSnapshot) GetResourceId() int64 {
if x != nil {
return x.ResourceId
}
return 0
}
func (x *RoomEntryVehicleSnapshot) GetResourceCode() string {
if x != nil {
return x.ResourceCode
}
return ""
}
func (x *RoomEntryVehicleSnapshot) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *RoomEntryVehicleSnapshot) GetAssetUrl() string {
if x != nil {
return x.AssetUrl
}
return ""
}
func (x *RoomEntryVehicleSnapshot) GetPreviewUrl() string {
if x != nil {
return x.PreviewUrl
}
return ""
}
func (x *RoomEntryVehicleSnapshot) GetAnimationUrl() string {
if x != nil {
return x.AnimationUrl
}
return ""
}
func (x *RoomEntryVehicleSnapshot) GetMetadataJson() string {
if x != nil {
return x.MetadataJson
}
return ""
}
func (x *RoomEntryVehicleSnapshot) GetEntitlementId() string {
if x != nil {
return x.EntitlementId
}
return ""
}
func (x *RoomEntryVehicleSnapshot) GetExpiresAtMs() int64 {
if x != nil {
return x.ExpiresAtMs
}
return 0
}
type RoomUserJoined struct {
state protoimpl.MessageState `protogen:"open.v1"`
UserId int64 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
Role string `protobuf:"bytes,2,opt,name=role,proto3" json:"role,omitempty"`
// visible_region_id 是用户进房时所在房间的国家/区域桶,统计服务不能反查 room-service。
VisibleRegionId int64 `protobuf:"varint,3,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
EntryVehicle *RoomEntryVehicleSnapshot `protobuf:"bytes,4,opt,name=entry_vehicle,json=entryVehicle,proto3" json:"entry_vehicle,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RoomUserJoined) Reset() {
*x = RoomUserJoined{}
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)
}
@ -372,7 +481,7 @@ func (x *RoomUserJoined) String() string {
func (*RoomUserJoined) ProtoMessage() {}
func (x *RoomUserJoined) 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 {
@ -385,7 +494,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{4}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{5}
}
func (x *RoomUserJoined) GetUserId() int64 {
@ -409,6 +518,13 @@ func (x *RoomUserJoined) GetVisibleRegionId() int64 {
return 0
}
func (x *RoomUserJoined) GetEntryVehicle() *RoomEntryVehicleSnapshot {
if x != nil {
return x.EntryVehicle
}
return nil
}
// RoomUserLeft 表达用户离房成功。
type RoomUserLeft struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -419,7 +535,7 @@ type RoomUserLeft struct {
func (x *RoomUserLeft) Reset() {
*x = RoomUserLeft{}
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)
}
@ -431,7 +547,7 @@ func (x *RoomUserLeft) String() string {
func (*RoomUserLeft) ProtoMessage() {}
func (x *RoomUserLeft) 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 {
@ -444,7 +560,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{5}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{6}
}
func (x *RoomUserLeft) GetUserId() int64 {
@ -465,7 +581,7 @@ type RoomClosed struct {
func (x *RoomClosed) Reset() {
*x = RoomClosed{}
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)
}
@ -477,7 +593,7 @@ func (x *RoomClosed) String() string {
func (*RoomClosed) ProtoMessage() {}
func (x *RoomClosed) 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 {
@ -490,7 +606,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{6}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{7}
}
func (x *RoomClosed) GetActorUserId() int64 {
@ -531,7 +647,7 @@ type RoomMicChanged struct {
func (x *RoomMicChanged) Reset() {
*x = RoomMicChanged{}
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)
}
@ -543,7 +659,7 @@ func (x *RoomMicChanged) String() string {
func (*RoomMicChanged) ProtoMessage() {}
func (x *RoomMicChanged) 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 {
@ -556,7 +672,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{7}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{8}
}
func (x *RoomMicChanged) GetActorUserId() int64 {
@ -655,7 +771,7 @@ type RoomMicSeatLocked struct {
func (x *RoomMicSeatLocked) Reset() {
*x = RoomMicSeatLocked{}
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)
}
@ -667,7 +783,7 @@ func (x *RoomMicSeatLocked) String() string {
func (*RoomMicSeatLocked) ProtoMessage() {}
func (x *RoomMicSeatLocked) 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 {
@ -680,7 +796,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{8}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{9}
}
func (x *RoomMicSeatLocked) GetActorUserId() int64 {
@ -715,7 +831,7 @@ type RoomChatEnabledChanged struct {
func (x *RoomChatEnabledChanged) Reset() {
*x = RoomChatEnabledChanged{}
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)
}
@ -727,7 +843,7 @@ func (x *RoomChatEnabledChanged) String() string {
func (*RoomChatEnabledChanged) ProtoMessage() {}
func (x *RoomChatEnabledChanged) 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 {
@ -740,7 +856,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{9}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10}
}
func (x *RoomChatEnabledChanged) GetActorUserId() int64 {
@ -768,7 +884,7 @@ type RoomPasswordChanged struct {
func (x *RoomPasswordChanged) Reset() {
*x = RoomPasswordChanged{}
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)
}
@ -780,7 +896,7 @@ func (x *RoomPasswordChanged) String() string {
func (*RoomPasswordChanged) ProtoMessage() {}
func (x *RoomPasswordChanged) 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 {
@ -793,7 +909,7 @@ func (x *RoomPasswordChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomPasswordChanged.ProtoReflect.Descriptor instead.
func (*RoomPasswordChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{10}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{11}
}
func (x *RoomPasswordChanged) GetActorUserId() int64 {
@ -822,7 +938,7 @@ type RoomAdminChanged struct {
func (x *RoomAdminChanged) Reset() {
*x = RoomAdminChanged{}
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)
}
@ -834,7 +950,7 @@ func (x *RoomAdminChanged) String() string {
func (*RoomAdminChanged) ProtoMessage() {}
func (x *RoomAdminChanged) 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 {
@ -847,7 +963,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{11}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{12}
}
func (x *RoomAdminChanged) GetActorUserId() int64 {
@ -883,7 +999,7 @@ type RoomUserMuted struct {
func (x *RoomUserMuted) Reset() {
*x = RoomUserMuted{}
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)
}
@ -895,7 +1011,7 @@ func (x *RoomUserMuted) String() string {
func (*RoomUserMuted) ProtoMessage() {}
func (x *RoomUserMuted) 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 {
@ -908,7 +1024,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{12}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{13}
}
func (x *RoomUserMuted) GetActorUserId() int64 {
@ -943,7 +1059,7 @@ type RoomUserKicked struct {
func (x *RoomUserKicked) Reset() {
*x = RoomUserKicked{}
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)
}
@ -955,7 +1071,7 @@ func (x *RoomUserKicked) String() string {
func (*RoomUserKicked) ProtoMessage() {}
func (x *RoomUserKicked) 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 {
@ -968,7 +1084,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{13}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{14}
}
func (x *RoomUserKicked) GetActorUserId() int64 {
@ -996,7 +1112,7 @@ type RoomUserUnbanned struct {
func (x *RoomUserUnbanned) Reset() {
*x = RoomUserUnbanned{}
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)
}
@ -1008,7 +1124,7 @@ func (x *RoomUserUnbanned) String() string {
func (*RoomUserUnbanned) ProtoMessage() {}
func (x *RoomUserUnbanned) 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 {
@ -1021,7 +1137,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{14}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{15}
}
func (x *RoomUserUnbanned) GetActorUserId() int64 {
@ -1056,7 +1172,7 @@ type RoomGiftSent struct {
func (x *RoomGiftSent) Reset() {
*x = RoomGiftSent{}
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)
}
@ -1068,7 +1184,7 @@ func (x *RoomGiftSent) String() string {
func (*RoomGiftSent) ProtoMessage() {}
func (x *RoomGiftSent) 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 {
@ -1081,7 +1197,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{15}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{16}
}
func (x *RoomGiftSent) GetSenderUserId() int64 {
@ -1158,7 +1274,7 @@ type RoomHeatChanged struct {
func (x *RoomHeatChanged) Reset() {
*x = RoomHeatChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1170,7 +1286,7 @@ func (x *RoomHeatChanged) String() string {
func (*RoomHeatChanged) ProtoMessage() {}
func (x *RoomHeatChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[16]
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1183,7 +1299,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{16}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17}
}
func (x *RoomHeatChanged) GetDelta() int64 {
@ -1212,7 +1328,7 @@ type RoomRankChanged struct {
func (x *RoomRankChanged) Reset() {
*x = RoomRankChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1224,7 +1340,7 @@ func (x *RoomRankChanged) String() string {
func (*RoomRankChanged) ProtoMessage() {}
func (x *RoomRankChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1237,7 +1353,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{17}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18}
}
func (x *RoomRankChanged) GetUserId() int64 {
@ -1277,7 +1393,7 @@ type RoomTreasureRewardGrant struct {
func (x *RoomTreasureRewardGrant) Reset() {
*x = RoomTreasureRewardGrant{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1289,7 +1405,7 @@ func (x *RoomTreasureRewardGrant) String() string {
func (*RoomTreasureRewardGrant) ProtoMessage() {}
func (x *RoomTreasureRewardGrant) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[18]
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1302,7 +1418,7 @@ func (x *RoomTreasureRewardGrant) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureRewardGrant.ProtoReflect.Descriptor instead.
func (*RoomTreasureRewardGrant) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
}
func (x *RoomTreasureRewardGrant) GetRewardRole() string {
@ -1384,7 +1500,7 @@ type RoomTreasureProgressChanged struct {
func (x *RoomTreasureProgressChanged) Reset() {
*x = RoomTreasureProgressChanged{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1396,7 +1512,7 @@ func (x *RoomTreasureProgressChanged) String() string {
func (*RoomTreasureProgressChanged) ProtoMessage() {}
func (x *RoomTreasureProgressChanged) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[19]
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1409,7 +1525,7 @@ func (x *RoomTreasureProgressChanged) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureProgressChanged.ProtoReflect.Descriptor instead.
func (*RoomTreasureProgressChanged) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
}
func (x *RoomTreasureProgressChanged) GetBoxId() string {
@ -1530,7 +1646,7 @@ type RoomTreasureCountdownStarted struct {
func (x *RoomTreasureCountdownStarted) Reset() {
*x = RoomTreasureCountdownStarted{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1542,7 +1658,7 @@ func (x *RoomTreasureCountdownStarted) String() string {
func (*RoomTreasureCountdownStarted) ProtoMessage() {}
func (x *RoomTreasureCountdownStarted) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[20]
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1555,7 +1671,7 @@ func (x *RoomTreasureCountdownStarted) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureCountdownStarted.ProtoReflect.Descriptor instead.
func (*RoomTreasureCountdownStarted) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
}
func (x *RoomTreasureCountdownStarted) GetBoxId() string {
@ -1653,7 +1769,7 @@ type RoomTreasureOpened struct {
func (x *RoomTreasureOpened) Reset() {
*x = RoomTreasureOpened{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1665,7 +1781,7 @@ func (x *RoomTreasureOpened) String() string {
func (*RoomTreasureOpened) ProtoMessage() {}
func (x *RoomTreasureOpened) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[21]
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1678,7 +1794,7 @@ func (x *RoomTreasureOpened) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureOpened.ProtoReflect.Descriptor instead.
func (*RoomTreasureOpened) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22}
}
func (x *RoomTreasureOpened) GetBoxId() string {
@ -1756,7 +1872,7 @@ type RoomTreasureRewardGranted struct {
func (x *RoomTreasureRewardGranted) Reset() {
*x = RoomTreasureRewardGranted{}
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1768,7 +1884,7 @@ func (x *RoomTreasureRewardGranted) String() string {
func (*RoomTreasureRewardGranted) ProtoMessage() {}
func (x *RoomTreasureRewardGranted) ProtoReflect() protoreflect.Message {
mi := &file_proto_events_room_v1_events_proto_msgTypes[22]
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1781,7 +1897,7 @@ func (x *RoomTreasureRewardGranted) ProtoReflect() protoreflect.Message {
// Deprecated: Use RoomTreasureRewardGranted.ProtoReflect.Descriptor instead.
func (*RoomTreasureRewardGranted) Descriptor() ([]byte, []int) {
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22}
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{23}
}
func (x *RoomTreasureRewardGranted) GetBoxId() string {
@ -1840,11 +1956,24 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
"\x15RoomBackgroundChanged\x12\"\n" +
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12#\n" +
"\rbackground_id\x18\x02 \x01(\x03R\fbackgroundId\x12.\n" +
"\x13room_background_url\x18\x03 \x01(\tR\x11roomBackgroundUrl\"i\n" +
"\x13room_background_url\x18\x03 \x01(\tR\x11roomBackgroundUrl\"\xc7\x02\n" +
"\x18RoomEntryVehicleSnapshot\x12\x1f\n" +
"\vresource_id\x18\x01 \x01(\x03R\n" +
"resourceId\x12#\n" +
"\rresource_code\x18\x02 \x01(\tR\fresourceCode\x12\x12\n" +
"\x04name\x18\x03 \x01(\tR\x04name\x12\x1b\n" +
"\tasset_url\x18\x04 \x01(\tR\bassetUrl\x12\x1f\n" +
"\vpreview_url\x18\x05 \x01(\tR\n" +
"previewUrl\x12#\n" +
"\ranimation_url\x18\x06 \x01(\tR\fanimationUrl\x12#\n" +
"\rmetadata_json\x18\a \x01(\tR\fmetadataJson\x12%\n" +
"\x0eentitlement_id\x18\b \x01(\tR\rentitlementId\x12\"\n" +
"\rexpires_at_ms\x18\t \x01(\x03R\vexpiresAtMs\"\xbe\x01\n" +
"\x0eRoomUserJoined\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\x12\x12\n" +
"\x04role\x18\x02 \x01(\tR\x04role\x12*\n" +
"\x11visible_region_id\x18\x03 \x01(\x03R\x0fvisibleRegionId\"'\n" +
"\x11visible_region_id\x18\x03 \x01(\x03R\x0fvisibleRegionId\x12S\n" +
"\rentry_vehicle\x18\x04 \x01(\v2..hyapp.events.room.v1.RoomEntryVehicleSnapshotR\fentryVehicle\"'\n" +
"\fRoomUserLeft\x12\x17\n" +
"\auser_id\x18\x01 \x01(\x03R\x06userId\"H\n" +
"\n" +
@ -1985,40 +2114,42 @@ 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, 23)
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 24)
var file_proto_events_room_v1_events_proto_goTypes = []any{
(*EventEnvelope)(nil), // 0: hyapp.events.room.v1.EventEnvelope
(*RoomCreated)(nil), // 1: hyapp.events.room.v1.RoomCreated
(*RoomProfileUpdated)(nil), // 2: hyapp.events.room.v1.RoomProfileUpdated
(*RoomBackgroundChanged)(nil), // 3: hyapp.events.room.v1.RoomBackgroundChanged
(*RoomUserJoined)(nil), // 4: hyapp.events.room.v1.RoomUserJoined
(*RoomUserLeft)(nil), // 5: hyapp.events.room.v1.RoomUserLeft
(*RoomClosed)(nil), // 6: hyapp.events.room.v1.RoomClosed
(*RoomMicChanged)(nil), // 7: hyapp.events.room.v1.RoomMicChanged
(*RoomMicSeatLocked)(nil), // 8: hyapp.events.room.v1.RoomMicSeatLocked
(*RoomChatEnabledChanged)(nil), // 9: hyapp.events.room.v1.RoomChatEnabledChanged
(*RoomPasswordChanged)(nil), // 10: hyapp.events.room.v1.RoomPasswordChanged
(*RoomAdminChanged)(nil), // 11: hyapp.events.room.v1.RoomAdminChanged
(*RoomUserMuted)(nil), // 12: hyapp.events.room.v1.RoomUserMuted
(*RoomUserKicked)(nil), // 13: hyapp.events.room.v1.RoomUserKicked
(*RoomUserUnbanned)(nil), // 14: hyapp.events.room.v1.RoomUserUnbanned
(*RoomGiftSent)(nil), // 15: hyapp.events.room.v1.RoomGiftSent
(*RoomHeatChanged)(nil), // 16: hyapp.events.room.v1.RoomHeatChanged
(*RoomRankChanged)(nil), // 17: hyapp.events.room.v1.RoomRankChanged
(*RoomTreasureRewardGrant)(nil), // 18: hyapp.events.room.v1.RoomTreasureRewardGrant
(*RoomTreasureProgressChanged)(nil), // 19: hyapp.events.room.v1.RoomTreasureProgressChanged
(*RoomTreasureCountdownStarted)(nil), // 20: hyapp.events.room.v1.RoomTreasureCountdownStarted
(*RoomTreasureOpened)(nil), // 21: hyapp.events.room.v1.RoomTreasureOpened
(*RoomTreasureRewardGranted)(nil), // 22: hyapp.events.room.v1.RoomTreasureRewardGranted
(*RoomEntryVehicleSnapshot)(nil), // 4: hyapp.events.room.v1.RoomEntryVehicleSnapshot
(*RoomUserJoined)(nil), // 5: hyapp.events.room.v1.RoomUserJoined
(*RoomUserLeft)(nil), // 6: hyapp.events.room.v1.RoomUserLeft
(*RoomClosed)(nil), // 7: hyapp.events.room.v1.RoomClosed
(*RoomMicChanged)(nil), // 8: hyapp.events.room.v1.RoomMicChanged
(*RoomMicSeatLocked)(nil), // 9: hyapp.events.room.v1.RoomMicSeatLocked
(*RoomChatEnabledChanged)(nil), // 10: hyapp.events.room.v1.RoomChatEnabledChanged
(*RoomPasswordChanged)(nil), // 11: hyapp.events.room.v1.RoomPasswordChanged
(*RoomAdminChanged)(nil), // 12: hyapp.events.room.v1.RoomAdminChanged
(*RoomUserMuted)(nil), // 13: hyapp.events.room.v1.RoomUserMuted
(*RoomUserKicked)(nil), // 14: hyapp.events.room.v1.RoomUserKicked
(*RoomUserUnbanned)(nil), // 15: hyapp.events.room.v1.RoomUserUnbanned
(*RoomGiftSent)(nil), // 16: hyapp.events.room.v1.RoomGiftSent
(*RoomHeatChanged)(nil), // 17: hyapp.events.room.v1.RoomHeatChanged
(*RoomRankChanged)(nil), // 18: hyapp.events.room.v1.RoomRankChanged
(*RoomTreasureRewardGrant)(nil), // 19: hyapp.events.room.v1.RoomTreasureRewardGrant
(*RoomTreasureProgressChanged)(nil), // 20: hyapp.events.room.v1.RoomTreasureProgressChanged
(*RoomTreasureCountdownStarted)(nil), // 21: hyapp.events.room.v1.RoomTreasureCountdownStarted
(*RoomTreasureOpened)(nil), // 22: hyapp.events.room.v1.RoomTreasureOpened
(*RoomTreasureRewardGranted)(nil), // 23: hyapp.events.room.v1.RoomTreasureRewardGranted
}
var file_proto_events_room_v1_events_proto_depIdxs = []int32{
18, // 0: hyapp.events.room.v1.RoomTreasureOpened.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
18, // 1: hyapp.events.room.v1.RoomTreasureRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
4, // 0: hyapp.events.room.v1.RoomUserJoined.entry_vehicle:type_name -> hyapp.events.room.v1.RoomEntryVehicleSnapshot
19, // 1: hyapp.events.room.v1.RoomTreasureOpened.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
19, // 2: hyapp.events.room.v1.RoomTreasureRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomTreasureRewardGrant
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_proto_events_room_v1_events_proto_init() }
@ -2032,7 +2163,7 @@ func file_proto_events_room_v1_events_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_events_room_v1_events_proto_rawDesc), len(file_proto_events_room_v1_events_proto_rawDesc)),
NumEnums: 0,
NumMessages: 23,
NumMessages: 24,
NumExtensions: 0,
NumServices: 0,
},

View File

@ -45,11 +45,24 @@ message RoomBackgroundChanged {
}
// RoomUserJoined
message RoomEntryVehicleSnapshot {
int64 resource_id = 1;
string resource_code = 2;
string name = 3;
string asset_url = 4;
string preview_url = 5;
string animation_url = 6;
string metadata_json = 7;
string entitlement_id = 8;
int64 expires_at_ms = 9;
}
message RoomUserJoined {
int64 user_id = 1;
string role = 2;
// visible_region_id / room-service
int64 visible_region_id = 3;
RoomEntryVehicleSnapshot entry_vehicle = 4;
}
// RoomUserLeft

File diff suppressed because it is too large Load Diff

View File

@ -426,11 +426,24 @@ message UpdateRoomProfileResponse {
}
// JoinRoomRequest presence
message RoomEntryVehicleSnapshot {
int64 resource_id = 1;
string resource_code = 2;
string name = 3;
string asset_url = 4;
string preview_url = 5;
string animation_url = 6;
string metadata_json = 7;
string entitlement_id = 8;
int64 expires_at_ms = 9;
}
message JoinRoomRequest {
RequestMeta meta = 1;
string role = 2;
// password room-service command log
string password = 3;
RoomEntryVehicleSnapshot entry_vehicle = 4;
}
// JoinRoomResponse

View File

@ -2556,6 +2556,102 @@ func (x *GetUserRoleSummaryResponse) GetSummary() *UserRoleSummary {
return nil
}
type GetAgencyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
AgencyId int64 `protobuf:"varint,2,opt,name=agency_id,json=agencyId,proto3" json:"agency_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAgencyRequest) Reset() {
*x = GetAgencyRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAgencyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAgencyRequest) ProtoMessage() {}
func (x *GetAgencyRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[33]
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 GetAgencyRequest.ProtoReflect.Descriptor instead.
func (*GetAgencyRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{33}
}
func (x *GetAgencyRequest) GetMeta() *RequestMeta {
if x != nil {
return x.Meta
}
return nil
}
func (x *GetAgencyRequest) GetAgencyId() int64 {
if x != nil {
return x.AgencyId
}
return 0
}
type GetAgencyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Agency *Agency `protobuf:"bytes,1,opt,name=agency,proto3" json:"agency,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAgencyResponse) Reset() {
*x = GetAgencyResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAgencyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAgencyResponse) ProtoMessage() {}
func (x *GetAgencyResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[34]
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 GetAgencyResponse.ProtoReflect.Descriptor instead.
func (*GetAgencyResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{34}
}
func (x *GetAgencyResponse) GetAgency() *Agency {
if x != nil {
return x.Agency
}
return nil
}
type CheckBusinessCapabilityRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
@ -2567,7 +2663,7 @@ type CheckBusinessCapabilityRequest struct {
func (x *CheckBusinessCapabilityRequest) Reset() {
*x = CheckBusinessCapabilityRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[33]
mi := &file_proto_user_v1_host_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2579,7 +2675,7 @@ func (x *CheckBusinessCapabilityRequest) String() string {
func (*CheckBusinessCapabilityRequest) ProtoMessage() {}
func (x *CheckBusinessCapabilityRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[33]
mi := &file_proto_user_v1_host_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2592,7 +2688,7 @@ func (x *CheckBusinessCapabilityRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CheckBusinessCapabilityRequest.ProtoReflect.Descriptor instead.
func (*CheckBusinessCapabilityRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{33}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{35}
}
func (x *CheckBusinessCapabilityRequest) GetMeta() *RequestMeta {
@ -2626,7 +2722,7 @@ type CheckBusinessCapabilityResponse struct {
func (x *CheckBusinessCapabilityResponse) Reset() {
*x = CheckBusinessCapabilityResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[34]
mi := &file_proto_user_v1_host_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2638,7 +2734,7 @@ func (x *CheckBusinessCapabilityResponse) String() string {
func (*CheckBusinessCapabilityResponse) ProtoMessage() {}
func (x *CheckBusinessCapabilityResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[34]
mi := &file_proto_user_v1_host_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2651,7 +2747,7 @@ func (x *CheckBusinessCapabilityResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CheckBusinessCapabilityResponse.ProtoReflect.Descriptor instead.
func (*CheckBusinessCapabilityResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{34}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{36}
}
func (x *CheckBusinessCapabilityResponse) GetAllowed() bool {
@ -2679,7 +2775,7 @@ type GetAgencyMembersRequest struct {
func (x *GetAgencyMembersRequest) Reset() {
*x = GetAgencyMembersRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[35]
mi := &file_proto_user_v1_host_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2691,7 +2787,7 @@ func (x *GetAgencyMembersRequest) String() string {
func (*GetAgencyMembersRequest) ProtoMessage() {}
func (x *GetAgencyMembersRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[35]
mi := &file_proto_user_v1_host_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2704,7 +2800,7 @@ func (x *GetAgencyMembersRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAgencyMembersRequest.ProtoReflect.Descriptor instead.
func (*GetAgencyMembersRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{35}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{37}
}
func (x *GetAgencyMembersRequest) GetMeta() *RequestMeta {
@ -2737,7 +2833,7 @@ type GetAgencyMembersResponse struct {
func (x *GetAgencyMembersResponse) Reset() {
*x = GetAgencyMembersResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[36]
mi := &file_proto_user_v1_host_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2749,7 +2845,7 @@ func (x *GetAgencyMembersResponse) String() string {
func (*GetAgencyMembersResponse) ProtoMessage() {}
func (x *GetAgencyMembersResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[36]
mi := &file_proto_user_v1_host_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2762,7 +2858,7 @@ func (x *GetAgencyMembersResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAgencyMembersResponse.ProtoReflect.Descriptor instead.
func (*GetAgencyMembersResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{36}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{38}
}
func (x *GetAgencyMembersResponse) GetMemberships() []*AgencyMembership {
@ -2783,7 +2879,7 @@ type GetAgencyApplicationsRequest struct {
func (x *GetAgencyApplicationsRequest) Reset() {
*x = GetAgencyApplicationsRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[37]
mi := &file_proto_user_v1_host_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2795,7 +2891,7 @@ func (x *GetAgencyApplicationsRequest) String() string {
func (*GetAgencyApplicationsRequest) ProtoMessage() {}
func (x *GetAgencyApplicationsRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[37]
mi := &file_proto_user_v1_host_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2808,7 +2904,7 @@ func (x *GetAgencyApplicationsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAgencyApplicationsRequest.ProtoReflect.Descriptor instead.
func (*GetAgencyApplicationsRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{37}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{39}
}
func (x *GetAgencyApplicationsRequest) GetMeta() *RequestMeta {
@ -2841,7 +2937,7 @@ type GetAgencyApplicationsResponse struct {
func (x *GetAgencyApplicationsResponse) Reset() {
*x = GetAgencyApplicationsResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[38]
mi := &file_proto_user_v1_host_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2853,7 +2949,7 @@ func (x *GetAgencyApplicationsResponse) String() string {
func (*GetAgencyApplicationsResponse) ProtoMessage() {}
func (x *GetAgencyApplicationsResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[38]
mi := &file_proto_user_v1_host_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2866,7 +2962,7 @@ func (x *GetAgencyApplicationsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use GetAgencyApplicationsResponse.ProtoReflect.Descriptor instead.
func (*GetAgencyApplicationsResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{38}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{40}
}
func (x *GetAgencyApplicationsResponse) GetApplications() []*AgencyApplication {
@ -2890,7 +2986,7 @@ type CreateBDLeaderRequest struct {
func (x *CreateBDLeaderRequest) Reset() {
*x = CreateBDLeaderRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[39]
mi := &file_proto_user_v1_host_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2902,7 +2998,7 @@ func (x *CreateBDLeaderRequest) String() string {
func (*CreateBDLeaderRequest) ProtoMessage() {}
func (x *CreateBDLeaderRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[39]
mi := &file_proto_user_v1_host_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2915,7 +3011,7 @@ func (x *CreateBDLeaderRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateBDLeaderRequest.ProtoReflect.Descriptor instead.
func (*CreateBDLeaderRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{39}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{41}
}
func (x *CreateBDLeaderRequest) GetMeta() *RequestMeta {
@ -2969,7 +3065,7 @@ type CreateBDLeaderResponse struct {
func (x *CreateBDLeaderResponse) Reset() {
*x = CreateBDLeaderResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[40]
mi := &file_proto_user_v1_host_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -2981,7 +3077,7 @@ func (x *CreateBDLeaderResponse) String() string {
func (*CreateBDLeaderResponse) ProtoMessage() {}
func (x *CreateBDLeaderResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[40]
mi := &file_proto_user_v1_host_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -2994,7 +3090,7 @@ func (x *CreateBDLeaderResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateBDLeaderResponse.ProtoReflect.Descriptor instead.
func (*CreateBDLeaderResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{40}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{42}
}
func (x *CreateBDLeaderResponse) GetBdProfile() *BDProfile {
@ -3018,7 +3114,7 @@ type CreateBDRequest struct {
func (x *CreateBDRequest) Reset() {
*x = CreateBDRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[41]
mi := &file_proto_user_v1_host_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3030,7 +3126,7 @@ func (x *CreateBDRequest) String() string {
func (*CreateBDRequest) ProtoMessage() {}
func (x *CreateBDRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[41]
mi := &file_proto_user_v1_host_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3043,7 +3139,7 @@ func (x *CreateBDRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateBDRequest.ProtoReflect.Descriptor instead.
func (*CreateBDRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{41}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{43}
}
func (x *CreateBDRequest) GetMeta() *RequestMeta {
@ -3097,7 +3193,7 @@ type CreateBDResponse struct {
func (x *CreateBDResponse) Reset() {
*x = CreateBDResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[42]
mi := &file_proto_user_v1_host_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3109,7 +3205,7 @@ func (x *CreateBDResponse) String() string {
func (*CreateBDResponse) ProtoMessage() {}
func (x *CreateBDResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[42]
mi := &file_proto_user_v1_host_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3122,7 +3218,7 @@ func (x *CreateBDResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateBDResponse.ProtoReflect.Descriptor instead.
func (*CreateBDResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{42}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{44}
}
func (x *CreateBDResponse) GetBdProfile() *BDProfile {
@ -3146,7 +3242,7 @@ type SetBDStatusRequest struct {
func (x *SetBDStatusRequest) Reset() {
*x = SetBDStatusRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[43]
mi := &file_proto_user_v1_host_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3158,7 +3254,7 @@ func (x *SetBDStatusRequest) String() string {
func (*SetBDStatusRequest) ProtoMessage() {}
func (x *SetBDStatusRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[43]
mi := &file_proto_user_v1_host_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3171,7 +3267,7 @@ func (x *SetBDStatusRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetBDStatusRequest.ProtoReflect.Descriptor instead.
func (*SetBDStatusRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{43}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{45}
}
func (x *SetBDStatusRequest) GetMeta() *RequestMeta {
@ -3225,7 +3321,7 @@ type SetBDStatusResponse struct {
func (x *SetBDStatusResponse) Reset() {
*x = SetBDStatusResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[44]
mi := &file_proto_user_v1_host_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3237,7 +3333,7 @@ func (x *SetBDStatusResponse) String() string {
func (*SetBDStatusResponse) ProtoMessage() {}
func (x *SetBDStatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[44]
mi := &file_proto_user_v1_host_proto_msgTypes[46]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3250,7 +3346,7 @@ func (x *SetBDStatusResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetBDStatusResponse.ProtoReflect.Descriptor instead.
func (*SetBDStatusResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{44}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{46}
}
func (x *SetBDStatusResponse) GetBdProfile() *BDProfile {
@ -3273,7 +3369,7 @@ type CreateCoinSellerRequest struct {
func (x *CreateCoinSellerRequest) Reset() {
*x = CreateCoinSellerRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[45]
mi := &file_proto_user_v1_host_proto_msgTypes[47]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3285,7 +3381,7 @@ func (x *CreateCoinSellerRequest) String() string {
func (*CreateCoinSellerRequest) ProtoMessage() {}
func (x *CreateCoinSellerRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[45]
mi := &file_proto_user_v1_host_proto_msgTypes[47]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3298,7 +3394,7 @@ func (x *CreateCoinSellerRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateCoinSellerRequest.ProtoReflect.Descriptor instead.
func (*CreateCoinSellerRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{45}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{47}
}
func (x *CreateCoinSellerRequest) GetMeta() *RequestMeta {
@ -3345,7 +3441,7 @@ type CreateCoinSellerResponse struct {
func (x *CreateCoinSellerResponse) Reset() {
*x = CreateCoinSellerResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[46]
mi := &file_proto_user_v1_host_proto_msgTypes[48]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3357,7 +3453,7 @@ func (x *CreateCoinSellerResponse) String() string {
func (*CreateCoinSellerResponse) ProtoMessage() {}
func (x *CreateCoinSellerResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[46]
mi := &file_proto_user_v1_host_proto_msgTypes[48]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3370,7 +3466,7 @@ func (x *CreateCoinSellerResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateCoinSellerResponse.ProtoReflect.Descriptor instead.
func (*CreateCoinSellerResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{46}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{48}
}
func (x *CreateCoinSellerResponse) GetCoinSellerProfile() *CoinSellerProfile {
@ -3394,7 +3490,7 @@ type SetCoinSellerStatusRequest struct {
func (x *SetCoinSellerStatusRequest) Reset() {
*x = SetCoinSellerStatusRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[47]
mi := &file_proto_user_v1_host_proto_msgTypes[49]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3406,7 +3502,7 @@ func (x *SetCoinSellerStatusRequest) String() string {
func (*SetCoinSellerStatusRequest) ProtoMessage() {}
func (x *SetCoinSellerStatusRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[47]
mi := &file_proto_user_v1_host_proto_msgTypes[49]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3419,7 +3515,7 @@ func (x *SetCoinSellerStatusRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetCoinSellerStatusRequest.ProtoReflect.Descriptor instead.
func (*SetCoinSellerStatusRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{47}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{49}
}
func (x *SetCoinSellerStatusRequest) GetMeta() *RequestMeta {
@ -3473,7 +3569,7 @@ type SetCoinSellerStatusResponse struct {
func (x *SetCoinSellerStatusResponse) Reset() {
*x = SetCoinSellerStatusResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[48]
mi := &file_proto_user_v1_host_proto_msgTypes[50]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3485,7 +3581,7 @@ func (x *SetCoinSellerStatusResponse) String() string {
func (*SetCoinSellerStatusResponse) ProtoMessage() {}
func (x *SetCoinSellerStatusResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[48]
mi := &file_proto_user_v1_host_proto_msgTypes[50]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3498,7 +3594,7 @@ func (x *SetCoinSellerStatusResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetCoinSellerStatusResponse.ProtoReflect.Descriptor instead.
func (*SetCoinSellerStatusResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{48}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{50}
}
func (x *SetCoinSellerStatusResponse) GetCoinSellerProfile() *CoinSellerProfile {
@ -3525,7 +3621,7 @@ type CreateAgencyRequest struct {
func (x *CreateAgencyRequest) Reset() {
*x = CreateAgencyRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[49]
mi := &file_proto_user_v1_host_proto_msgTypes[51]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3537,7 +3633,7 @@ func (x *CreateAgencyRequest) String() string {
func (*CreateAgencyRequest) ProtoMessage() {}
func (x *CreateAgencyRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[49]
mi := &file_proto_user_v1_host_proto_msgTypes[51]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3550,7 +3646,7 @@ func (x *CreateAgencyRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateAgencyRequest.ProtoReflect.Descriptor instead.
func (*CreateAgencyRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{49}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{51}
}
func (x *CreateAgencyRequest) GetMeta() *RequestMeta {
@ -3627,7 +3723,7 @@ type CreateAgencyResponse struct {
func (x *CreateAgencyResponse) Reset() {
*x = CreateAgencyResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[50]
mi := &file_proto_user_v1_host_proto_msgTypes[52]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3639,7 +3735,7 @@ func (x *CreateAgencyResponse) String() string {
func (*CreateAgencyResponse) ProtoMessage() {}
func (x *CreateAgencyResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[50]
mi := &file_proto_user_v1_host_proto_msgTypes[52]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3652,7 +3748,7 @@ func (x *CreateAgencyResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CreateAgencyResponse.ProtoReflect.Descriptor instead.
func (*CreateAgencyResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{50}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{52}
}
func (x *CreateAgencyResponse) GetAgency() *Agency {
@ -3689,7 +3785,7 @@ type CloseAgencyRequest struct {
func (x *CloseAgencyRequest) Reset() {
*x = CloseAgencyRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[51]
mi := &file_proto_user_v1_host_proto_msgTypes[53]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3701,7 +3797,7 @@ func (x *CloseAgencyRequest) String() string {
func (*CloseAgencyRequest) ProtoMessage() {}
func (x *CloseAgencyRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[51]
mi := &file_proto_user_v1_host_proto_msgTypes[53]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3714,7 +3810,7 @@ func (x *CloseAgencyRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use CloseAgencyRequest.ProtoReflect.Descriptor instead.
func (*CloseAgencyRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{51}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{53}
}
func (x *CloseAgencyRequest) GetMeta() *RequestMeta {
@ -3761,7 +3857,7 @@ type CloseAgencyResponse struct {
func (x *CloseAgencyResponse) Reset() {
*x = CloseAgencyResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[52]
mi := &file_proto_user_v1_host_proto_msgTypes[54]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3773,7 +3869,7 @@ func (x *CloseAgencyResponse) String() string {
func (*CloseAgencyResponse) ProtoMessage() {}
func (x *CloseAgencyResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[52]
mi := &file_proto_user_v1_host_proto_msgTypes[54]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3786,7 +3882,7 @@ func (x *CloseAgencyResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use CloseAgencyResponse.ProtoReflect.Descriptor instead.
func (*CloseAgencyResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{52}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{54}
}
func (x *CloseAgencyResponse) GetAgency() *Agency {
@ -3810,7 +3906,7 @@ type SetAgencyJoinEnabledRequest struct {
func (x *SetAgencyJoinEnabledRequest) Reset() {
*x = SetAgencyJoinEnabledRequest{}
mi := &file_proto_user_v1_host_proto_msgTypes[53]
mi := &file_proto_user_v1_host_proto_msgTypes[55]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3822,7 +3918,7 @@ func (x *SetAgencyJoinEnabledRequest) String() string {
func (*SetAgencyJoinEnabledRequest) ProtoMessage() {}
func (x *SetAgencyJoinEnabledRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[53]
mi := &file_proto_user_v1_host_proto_msgTypes[55]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3835,7 +3931,7 @@ func (x *SetAgencyJoinEnabledRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetAgencyJoinEnabledRequest.ProtoReflect.Descriptor instead.
func (*SetAgencyJoinEnabledRequest) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{53}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{55}
}
func (x *SetAgencyJoinEnabledRequest) GetMeta() *RequestMeta {
@ -3889,7 +3985,7 @@ type SetAgencyJoinEnabledResponse struct {
func (x *SetAgencyJoinEnabledResponse) Reset() {
*x = SetAgencyJoinEnabledResponse{}
mi := &file_proto_user_v1_host_proto_msgTypes[54]
mi := &file_proto_user_v1_host_proto_msgTypes[56]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -3901,7 +3997,7 @@ func (x *SetAgencyJoinEnabledResponse) String() string {
func (*SetAgencyJoinEnabledResponse) ProtoMessage() {}
func (x *SetAgencyJoinEnabledResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_user_v1_host_proto_msgTypes[54]
mi := &file_proto_user_v1_host_proto_msgTypes[56]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -3914,7 +4010,7 @@ func (x *SetAgencyJoinEnabledResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetAgencyJoinEnabledResponse.ProtoReflect.Descriptor instead.
func (*SetAgencyJoinEnabledResponse) Descriptor() ([]byte, []int) {
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{54}
return file_proto_user_v1_host_proto_rawDescGZIP(), []int{56}
}
func (x *SetAgencyJoinEnabledResponse) GetAgency() *Agency {
@ -4169,7 +4265,12 @@ const file_proto_user_v1_host_proto_rawDesc = "" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x17\n" +
"\auser_id\x18\x02 \x01(\x03R\x06userId\"V\n" +
"\x1aGetUserRoleSummaryResponse\x128\n" +
"\asummary\x18\x01 \x01(\v2\x1e.hyapp.user.v1.UserRoleSummaryR\asummary\"\x94\x01\n" +
"\asummary\x18\x01 \x01(\v2\x1e.hyapp.user.v1.UserRoleSummaryR\asummary\"_\n" +
"\x10GetAgencyRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x1b\n" +
"\tagency_id\x18\x02 \x01(\x03R\bagencyId\"B\n" +
"\x11GetAgencyResponse\x12-\n" +
"\x06agency\x18\x01 \x01(\v2\x15.hyapp.user.v1.AgencyR\x06agency\"\x94\x01\n" +
"\x1eCheckBusinessCapabilityRequest\x12.\n" +
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\"\n" +
"\ractor_user_id\x18\x02 \x01(\x03R\vactorUserId\x12\x1e\n" +
@ -4278,7 +4379,7 @@ const file_proto_user_v1_host_proto_rawDesc = "" +
"\fjoin_enabled\x18\x05 \x01(\bR\vjoinEnabled\x12\x16\n" +
"\x06reason\x18\x06 \x01(\tR\x06reason\"M\n" +
"\x1cSetAgencyJoinEnabledResponse\x12-\n" +
"\x06agency\x18\x01 \x01(\v2\x15.hyapp.user.v1.AgencyR\x06agency2\xb9\f\n" +
"\x06agency\x18\x01 \x01(\v2\x15.hyapp.user.v1.AgencyR\x06agency2\x89\r\n" +
"\x0fUserHostService\x12]\n" +
"\x0eSearchAgencies\x12$.hyapp.user.v1.SearchAgenciesRequest\x1a%.hyapp.user.v1.SearchAgenciesResponse\x12Z\n" +
"\rApplyToAgency\x12#.hyapp.user.v1.ApplyToAgencyRequest\x1a$.hyapp.user.v1.ApplyToAgencyResponse\x12x\n" +
@ -4291,7 +4392,8 @@ const file_proto_user_v1_host_proto_rawDesc = "" +
"\fGetBDProfile\x12\".hyapp.user.v1.GetBDProfileRequest\x1a#.hyapp.user.v1.GetBDProfileResponse\x12o\n" +
"\x14GetCoinSellerProfile\x12*.hyapp.user.v1.GetCoinSellerProfileRequest\x1a+.hyapp.user.v1.GetCoinSellerProfileResponse\x12\x90\x01\n" +
"\x1fListActiveCoinSellersInMyRegion\x125.hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest\x1a6.hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse\x12i\n" +
"\x12GetUserRoleSummary\x12(.hyapp.user.v1.GetUserRoleSummaryRequest\x1a).hyapp.user.v1.GetUserRoleSummaryResponse\x12x\n" +
"\x12GetUserRoleSummary\x12(.hyapp.user.v1.GetUserRoleSummaryRequest\x1a).hyapp.user.v1.GetUserRoleSummaryResponse\x12N\n" +
"\tGetAgency\x12\x1f.hyapp.user.v1.GetAgencyRequest\x1a .hyapp.user.v1.GetAgencyResponse\x12x\n" +
"\x17CheckBusinessCapability\x12-.hyapp.user.v1.CheckBusinessCapabilityRequest\x1a..hyapp.user.v1.CheckBusinessCapabilityResponse\x12c\n" +
"\x10GetAgencyMembers\x12&.hyapp.user.v1.GetAgencyMembersRequest\x1a'.hyapp.user.v1.GetAgencyMembersResponse\x12r\n" +
"\x15GetAgencyApplications\x12+.hyapp.user.v1.GetAgencyApplicationsRequest\x1a,.hyapp.user.v1.GetAgencyApplicationsResponse2\x8b\x06\n" +
@ -4317,7 +4419,7 @@ func file_proto_user_v1_host_proto_rawDescGZIP() []byte {
return file_proto_user_v1_host_proto_rawDescData
}
var file_proto_user_v1_host_proto_msgTypes = make([]protoimpl.MessageInfo, 55)
var file_proto_user_v1_host_proto_msgTypes = make([]protoimpl.MessageInfo, 57)
var file_proto_user_v1_host_proto_goTypes = []any{
(*HostProfile)(nil), // 0: hyapp.user.v1.HostProfile
(*Agency)(nil), // 1: hyapp.user.v1.Agency
@ -4352,136 +4454,142 @@ var file_proto_user_v1_host_proto_goTypes = []any{
(*ListActiveCoinSellersInMyRegionResponse)(nil), // 30: hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse
(*GetUserRoleSummaryRequest)(nil), // 31: hyapp.user.v1.GetUserRoleSummaryRequest
(*GetUserRoleSummaryResponse)(nil), // 32: hyapp.user.v1.GetUserRoleSummaryResponse
(*CheckBusinessCapabilityRequest)(nil), // 33: hyapp.user.v1.CheckBusinessCapabilityRequest
(*CheckBusinessCapabilityResponse)(nil), // 34: hyapp.user.v1.CheckBusinessCapabilityResponse
(*GetAgencyMembersRequest)(nil), // 35: hyapp.user.v1.GetAgencyMembersRequest
(*GetAgencyMembersResponse)(nil), // 36: hyapp.user.v1.GetAgencyMembersResponse
(*GetAgencyApplicationsRequest)(nil), // 37: hyapp.user.v1.GetAgencyApplicationsRequest
(*GetAgencyApplicationsResponse)(nil), // 38: hyapp.user.v1.GetAgencyApplicationsResponse
(*CreateBDLeaderRequest)(nil), // 39: hyapp.user.v1.CreateBDLeaderRequest
(*CreateBDLeaderResponse)(nil), // 40: hyapp.user.v1.CreateBDLeaderResponse
(*CreateBDRequest)(nil), // 41: hyapp.user.v1.CreateBDRequest
(*CreateBDResponse)(nil), // 42: hyapp.user.v1.CreateBDResponse
(*SetBDStatusRequest)(nil), // 43: hyapp.user.v1.SetBDStatusRequest
(*SetBDStatusResponse)(nil), // 44: hyapp.user.v1.SetBDStatusResponse
(*CreateCoinSellerRequest)(nil), // 45: hyapp.user.v1.CreateCoinSellerRequest
(*CreateCoinSellerResponse)(nil), // 46: hyapp.user.v1.CreateCoinSellerResponse
(*SetCoinSellerStatusRequest)(nil), // 47: hyapp.user.v1.SetCoinSellerStatusRequest
(*SetCoinSellerStatusResponse)(nil), // 48: hyapp.user.v1.SetCoinSellerStatusResponse
(*CreateAgencyRequest)(nil), // 49: hyapp.user.v1.CreateAgencyRequest
(*CreateAgencyResponse)(nil), // 50: hyapp.user.v1.CreateAgencyResponse
(*CloseAgencyRequest)(nil), // 51: hyapp.user.v1.CloseAgencyRequest
(*CloseAgencyResponse)(nil), // 52: hyapp.user.v1.CloseAgencyResponse
(*SetAgencyJoinEnabledRequest)(nil), // 53: hyapp.user.v1.SetAgencyJoinEnabledRequest
(*SetAgencyJoinEnabledResponse)(nil), // 54: hyapp.user.v1.SetAgencyJoinEnabledResponse
(*RequestMeta)(nil), // 55: hyapp.user.v1.RequestMeta
(*GetAgencyRequest)(nil), // 33: hyapp.user.v1.GetAgencyRequest
(*GetAgencyResponse)(nil), // 34: hyapp.user.v1.GetAgencyResponse
(*CheckBusinessCapabilityRequest)(nil), // 35: hyapp.user.v1.CheckBusinessCapabilityRequest
(*CheckBusinessCapabilityResponse)(nil), // 36: hyapp.user.v1.CheckBusinessCapabilityResponse
(*GetAgencyMembersRequest)(nil), // 37: hyapp.user.v1.GetAgencyMembersRequest
(*GetAgencyMembersResponse)(nil), // 38: hyapp.user.v1.GetAgencyMembersResponse
(*GetAgencyApplicationsRequest)(nil), // 39: hyapp.user.v1.GetAgencyApplicationsRequest
(*GetAgencyApplicationsResponse)(nil), // 40: hyapp.user.v1.GetAgencyApplicationsResponse
(*CreateBDLeaderRequest)(nil), // 41: hyapp.user.v1.CreateBDLeaderRequest
(*CreateBDLeaderResponse)(nil), // 42: hyapp.user.v1.CreateBDLeaderResponse
(*CreateBDRequest)(nil), // 43: hyapp.user.v1.CreateBDRequest
(*CreateBDResponse)(nil), // 44: hyapp.user.v1.CreateBDResponse
(*SetBDStatusRequest)(nil), // 45: hyapp.user.v1.SetBDStatusRequest
(*SetBDStatusResponse)(nil), // 46: hyapp.user.v1.SetBDStatusResponse
(*CreateCoinSellerRequest)(nil), // 47: hyapp.user.v1.CreateCoinSellerRequest
(*CreateCoinSellerResponse)(nil), // 48: hyapp.user.v1.CreateCoinSellerResponse
(*SetCoinSellerStatusRequest)(nil), // 49: hyapp.user.v1.SetCoinSellerStatusRequest
(*SetCoinSellerStatusResponse)(nil), // 50: hyapp.user.v1.SetCoinSellerStatusResponse
(*CreateAgencyRequest)(nil), // 51: hyapp.user.v1.CreateAgencyRequest
(*CreateAgencyResponse)(nil), // 52: hyapp.user.v1.CreateAgencyResponse
(*CloseAgencyRequest)(nil), // 53: hyapp.user.v1.CloseAgencyRequest
(*CloseAgencyResponse)(nil), // 54: hyapp.user.v1.CloseAgencyResponse
(*SetAgencyJoinEnabledRequest)(nil), // 55: hyapp.user.v1.SetAgencyJoinEnabledRequest
(*SetAgencyJoinEnabledResponse)(nil), // 56: hyapp.user.v1.SetAgencyJoinEnabledResponse
(*RequestMeta)(nil), // 57: hyapp.user.v1.RequestMeta
}
var file_proto_user_v1_host_proto_depIdxs = []int32{
55, // 0: hyapp.user.v1.SearchAgenciesRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 0: hyapp.user.v1.SearchAgenciesRequest.meta:type_name -> hyapp.user.v1.RequestMeta
1, // 1: hyapp.user.v1.SearchAgenciesResponse.agencies:type_name -> hyapp.user.v1.Agency
55, // 2: hyapp.user.v1.ApplyToAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 2: hyapp.user.v1.ApplyToAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
7, // 3: hyapp.user.v1.ApplyToAgencyResponse.application:type_name -> hyapp.user.v1.AgencyApplication
55, // 4: hyapp.user.v1.ReviewAgencyApplicationRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 4: hyapp.user.v1.ReviewAgencyApplicationRequest.meta:type_name -> hyapp.user.v1.RequestMeta
7, // 5: hyapp.user.v1.ReviewAgencyApplicationResponse.application:type_name -> hyapp.user.v1.AgencyApplication
0, // 6: hyapp.user.v1.ReviewAgencyApplicationResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
6, // 7: hyapp.user.v1.ReviewAgencyApplicationResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
55, // 8: hyapp.user.v1.KickAgencyHostRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 8: hyapp.user.v1.KickAgencyHostRequest.meta:type_name -> hyapp.user.v1.RequestMeta
6, // 9: hyapp.user.v1.KickAgencyHostResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
0, // 10: hyapp.user.v1.KickAgencyHostResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
55, // 11: hyapp.user.v1.InviteAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 11: hyapp.user.v1.InviteAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
8, // 12: hyapp.user.v1.InviteAgencyResponse.invitation:type_name -> hyapp.user.v1.RoleInvitation
55, // 13: hyapp.user.v1.InviteBDRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 13: hyapp.user.v1.InviteBDRequest.meta:type_name -> hyapp.user.v1.RequestMeta
8, // 14: hyapp.user.v1.InviteBDResponse.invitation:type_name -> hyapp.user.v1.RoleInvitation
55, // 15: hyapp.user.v1.ProcessRoleInvitationRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 15: hyapp.user.v1.ProcessRoleInvitationRequest.meta:type_name -> hyapp.user.v1.RequestMeta
8, // 16: hyapp.user.v1.ProcessRoleInvitationResponse.invitation:type_name -> hyapp.user.v1.RoleInvitation
0, // 17: hyapp.user.v1.ProcessRoleInvitationResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
1, // 18: hyapp.user.v1.ProcessRoleInvitationResponse.agency:type_name -> hyapp.user.v1.Agency
6, // 19: hyapp.user.v1.ProcessRoleInvitationResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
2, // 20: hyapp.user.v1.ProcessRoleInvitationResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
55, // 21: hyapp.user.v1.GetHostProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 21: hyapp.user.v1.GetHostProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
0, // 22: hyapp.user.v1.GetHostProfileResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
55, // 23: hyapp.user.v1.GetBDProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 23: hyapp.user.v1.GetBDProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
2, // 24: hyapp.user.v1.GetBDProfileResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
55, // 25: hyapp.user.v1.GetCoinSellerProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 25: hyapp.user.v1.GetCoinSellerProfileRequest.meta:type_name -> hyapp.user.v1.RequestMeta
3, // 26: hyapp.user.v1.GetCoinSellerProfileResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
55, // 27: hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 27: hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest.meta:type_name -> hyapp.user.v1.RequestMeta
4, // 28: hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse.coin_sellers:type_name -> hyapp.user.v1.CoinSellerListItem
55, // 29: hyapp.user.v1.GetUserRoleSummaryRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 29: hyapp.user.v1.GetUserRoleSummaryRequest.meta:type_name -> hyapp.user.v1.RequestMeta
5, // 30: hyapp.user.v1.GetUserRoleSummaryResponse.summary:type_name -> hyapp.user.v1.UserRoleSummary
55, // 31: hyapp.user.v1.CheckBusinessCapabilityRequest.meta:type_name -> hyapp.user.v1.RequestMeta
55, // 32: hyapp.user.v1.GetAgencyMembersRequest.meta:type_name -> hyapp.user.v1.RequestMeta
6, // 33: hyapp.user.v1.GetAgencyMembersResponse.memberships:type_name -> hyapp.user.v1.AgencyMembership
55, // 34: hyapp.user.v1.GetAgencyApplicationsRequest.meta:type_name -> hyapp.user.v1.RequestMeta
7, // 35: hyapp.user.v1.GetAgencyApplicationsResponse.applications:type_name -> hyapp.user.v1.AgencyApplication
55, // 36: hyapp.user.v1.CreateBDLeaderRequest.meta:type_name -> hyapp.user.v1.RequestMeta
2, // 37: hyapp.user.v1.CreateBDLeaderResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
55, // 38: hyapp.user.v1.CreateBDRequest.meta:type_name -> hyapp.user.v1.RequestMeta
2, // 39: hyapp.user.v1.CreateBDResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
55, // 40: hyapp.user.v1.SetBDStatusRequest.meta:type_name -> hyapp.user.v1.RequestMeta
2, // 41: hyapp.user.v1.SetBDStatusResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
55, // 42: hyapp.user.v1.CreateCoinSellerRequest.meta:type_name -> hyapp.user.v1.RequestMeta
3, // 43: hyapp.user.v1.CreateCoinSellerResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
55, // 44: hyapp.user.v1.SetCoinSellerStatusRequest.meta:type_name -> hyapp.user.v1.RequestMeta
3, // 45: hyapp.user.v1.SetCoinSellerStatusResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
55, // 46: hyapp.user.v1.CreateAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
1, // 47: hyapp.user.v1.CreateAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
0, // 48: hyapp.user.v1.CreateAgencyResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
6, // 49: hyapp.user.v1.CreateAgencyResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
55, // 50: hyapp.user.v1.CloseAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
1, // 51: hyapp.user.v1.CloseAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
55, // 52: hyapp.user.v1.SetAgencyJoinEnabledRequest.meta:type_name -> hyapp.user.v1.RequestMeta
1, // 53: hyapp.user.v1.SetAgencyJoinEnabledResponse.agency:type_name -> hyapp.user.v1.Agency
9, // 54: hyapp.user.v1.UserHostService.SearchAgencies:input_type -> hyapp.user.v1.SearchAgenciesRequest
11, // 55: hyapp.user.v1.UserHostService.ApplyToAgency:input_type -> hyapp.user.v1.ApplyToAgencyRequest
13, // 56: hyapp.user.v1.UserHostService.ReviewAgencyApplication:input_type -> hyapp.user.v1.ReviewAgencyApplicationRequest
15, // 57: hyapp.user.v1.UserHostService.KickAgencyHost:input_type -> hyapp.user.v1.KickAgencyHostRequest
17, // 58: hyapp.user.v1.UserHostService.InviteAgency:input_type -> hyapp.user.v1.InviteAgencyRequest
19, // 59: hyapp.user.v1.UserHostService.InviteBD:input_type -> hyapp.user.v1.InviteBDRequest
21, // 60: hyapp.user.v1.UserHostService.ProcessRoleInvitation:input_type -> hyapp.user.v1.ProcessRoleInvitationRequest
23, // 61: hyapp.user.v1.UserHostService.GetHostProfile:input_type -> hyapp.user.v1.GetHostProfileRequest
25, // 62: hyapp.user.v1.UserHostService.GetBDProfile:input_type -> hyapp.user.v1.GetBDProfileRequest
27, // 63: hyapp.user.v1.UserHostService.GetCoinSellerProfile:input_type -> hyapp.user.v1.GetCoinSellerProfileRequest
29, // 64: hyapp.user.v1.UserHostService.ListActiveCoinSellersInMyRegion:input_type -> hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest
31, // 65: hyapp.user.v1.UserHostService.GetUserRoleSummary:input_type -> hyapp.user.v1.GetUserRoleSummaryRequest
33, // 66: hyapp.user.v1.UserHostService.CheckBusinessCapability:input_type -> hyapp.user.v1.CheckBusinessCapabilityRequest
35, // 67: hyapp.user.v1.UserHostService.GetAgencyMembers:input_type -> hyapp.user.v1.GetAgencyMembersRequest
37, // 68: hyapp.user.v1.UserHostService.GetAgencyApplications:input_type -> hyapp.user.v1.GetAgencyApplicationsRequest
39, // 69: hyapp.user.v1.UserHostAdminService.CreateBDLeader:input_type -> hyapp.user.v1.CreateBDLeaderRequest
41, // 70: hyapp.user.v1.UserHostAdminService.CreateBD:input_type -> hyapp.user.v1.CreateBDRequest
43, // 71: hyapp.user.v1.UserHostAdminService.SetBDStatus:input_type -> hyapp.user.v1.SetBDStatusRequest
45, // 72: hyapp.user.v1.UserHostAdminService.CreateCoinSeller:input_type -> hyapp.user.v1.CreateCoinSellerRequest
47, // 73: hyapp.user.v1.UserHostAdminService.SetCoinSellerStatus:input_type -> hyapp.user.v1.SetCoinSellerStatusRequest
49, // 74: hyapp.user.v1.UserHostAdminService.CreateAgency:input_type -> hyapp.user.v1.CreateAgencyRequest
51, // 75: hyapp.user.v1.UserHostAdminService.CloseAgency:input_type -> hyapp.user.v1.CloseAgencyRequest
53, // 76: hyapp.user.v1.UserHostAdminService.SetAgencyJoinEnabled:input_type -> hyapp.user.v1.SetAgencyJoinEnabledRequest
10, // 77: hyapp.user.v1.UserHostService.SearchAgencies:output_type -> hyapp.user.v1.SearchAgenciesResponse
12, // 78: hyapp.user.v1.UserHostService.ApplyToAgency:output_type -> hyapp.user.v1.ApplyToAgencyResponse
14, // 79: hyapp.user.v1.UserHostService.ReviewAgencyApplication:output_type -> hyapp.user.v1.ReviewAgencyApplicationResponse
16, // 80: hyapp.user.v1.UserHostService.KickAgencyHost:output_type -> hyapp.user.v1.KickAgencyHostResponse
18, // 81: hyapp.user.v1.UserHostService.InviteAgency:output_type -> hyapp.user.v1.InviteAgencyResponse
20, // 82: hyapp.user.v1.UserHostService.InviteBD:output_type -> hyapp.user.v1.InviteBDResponse
22, // 83: hyapp.user.v1.UserHostService.ProcessRoleInvitation:output_type -> hyapp.user.v1.ProcessRoleInvitationResponse
24, // 84: hyapp.user.v1.UserHostService.GetHostProfile:output_type -> hyapp.user.v1.GetHostProfileResponse
26, // 85: hyapp.user.v1.UserHostService.GetBDProfile:output_type -> hyapp.user.v1.GetBDProfileResponse
28, // 86: hyapp.user.v1.UserHostService.GetCoinSellerProfile:output_type -> hyapp.user.v1.GetCoinSellerProfileResponse
30, // 87: hyapp.user.v1.UserHostService.ListActiveCoinSellersInMyRegion:output_type -> hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse
32, // 88: hyapp.user.v1.UserHostService.GetUserRoleSummary:output_type -> hyapp.user.v1.GetUserRoleSummaryResponse
34, // 89: hyapp.user.v1.UserHostService.CheckBusinessCapability:output_type -> hyapp.user.v1.CheckBusinessCapabilityResponse
36, // 90: hyapp.user.v1.UserHostService.GetAgencyMembers:output_type -> hyapp.user.v1.GetAgencyMembersResponse
38, // 91: hyapp.user.v1.UserHostService.GetAgencyApplications:output_type -> hyapp.user.v1.GetAgencyApplicationsResponse
40, // 92: hyapp.user.v1.UserHostAdminService.CreateBDLeader:output_type -> hyapp.user.v1.CreateBDLeaderResponse
42, // 93: hyapp.user.v1.UserHostAdminService.CreateBD:output_type -> hyapp.user.v1.CreateBDResponse
44, // 94: hyapp.user.v1.UserHostAdminService.SetBDStatus:output_type -> hyapp.user.v1.SetBDStatusResponse
46, // 95: hyapp.user.v1.UserHostAdminService.CreateCoinSeller:output_type -> hyapp.user.v1.CreateCoinSellerResponse
48, // 96: hyapp.user.v1.UserHostAdminService.SetCoinSellerStatus:output_type -> hyapp.user.v1.SetCoinSellerStatusResponse
50, // 97: hyapp.user.v1.UserHostAdminService.CreateAgency:output_type -> hyapp.user.v1.CreateAgencyResponse
52, // 98: hyapp.user.v1.UserHostAdminService.CloseAgency:output_type -> hyapp.user.v1.CloseAgencyResponse
54, // 99: hyapp.user.v1.UserHostAdminService.SetAgencyJoinEnabled:output_type -> hyapp.user.v1.SetAgencyJoinEnabledResponse
77, // [77:100] is the sub-list for method output_type
54, // [54:77] is the sub-list for method input_type
54, // [54:54] is the sub-list for extension type_name
54, // [54:54] is the sub-list for extension extendee
0, // [0:54] is the sub-list for field type_name
57, // 31: hyapp.user.v1.GetAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
1, // 32: hyapp.user.v1.GetAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
57, // 33: hyapp.user.v1.CheckBusinessCapabilityRequest.meta:type_name -> hyapp.user.v1.RequestMeta
57, // 34: hyapp.user.v1.GetAgencyMembersRequest.meta:type_name -> hyapp.user.v1.RequestMeta
6, // 35: hyapp.user.v1.GetAgencyMembersResponse.memberships:type_name -> hyapp.user.v1.AgencyMembership
57, // 36: hyapp.user.v1.GetAgencyApplicationsRequest.meta:type_name -> hyapp.user.v1.RequestMeta
7, // 37: hyapp.user.v1.GetAgencyApplicationsResponse.applications:type_name -> hyapp.user.v1.AgencyApplication
57, // 38: hyapp.user.v1.CreateBDLeaderRequest.meta:type_name -> hyapp.user.v1.RequestMeta
2, // 39: hyapp.user.v1.CreateBDLeaderResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
57, // 40: hyapp.user.v1.CreateBDRequest.meta:type_name -> hyapp.user.v1.RequestMeta
2, // 41: hyapp.user.v1.CreateBDResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
57, // 42: hyapp.user.v1.SetBDStatusRequest.meta:type_name -> hyapp.user.v1.RequestMeta
2, // 43: hyapp.user.v1.SetBDStatusResponse.bd_profile:type_name -> hyapp.user.v1.BDProfile
57, // 44: hyapp.user.v1.CreateCoinSellerRequest.meta:type_name -> hyapp.user.v1.RequestMeta
3, // 45: hyapp.user.v1.CreateCoinSellerResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
57, // 46: hyapp.user.v1.SetCoinSellerStatusRequest.meta:type_name -> hyapp.user.v1.RequestMeta
3, // 47: hyapp.user.v1.SetCoinSellerStatusResponse.coin_seller_profile:type_name -> hyapp.user.v1.CoinSellerProfile
57, // 48: hyapp.user.v1.CreateAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
1, // 49: hyapp.user.v1.CreateAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
0, // 50: hyapp.user.v1.CreateAgencyResponse.host_profile:type_name -> hyapp.user.v1.HostProfile
6, // 51: hyapp.user.v1.CreateAgencyResponse.membership:type_name -> hyapp.user.v1.AgencyMembership
57, // 52: hyapp.user.v1.CloseAgencyRequest.meta:type_name -> hyapp.user.v1.RequestMeta
1, // 53: hyapp.user.v1.CloseAgencyResponse.agency:type_name -> hyapp.user.v1.Agency
57, // 54: hyapp.user.v1.SetAgencyJoinEnabledRequest.meta:type_name -> hyapp.user.v1.RequestMeta
1, // 55: hyapp.user.v1.SetAgencyJoinEnabledResponse.agency:type_name -> hyapp.user.v1.Agency
9, // 56: hyapp.user.v1.UserHostService.SearchAgencies:input_type -> hyapp.user.v1.SearchAgenciesRequest
11, // 57: hyapp.user.v1.UserHostService.ApplyToAgency:input_type -> hyapp.user.v1.ApplyToAgencyRequest
13, // 58: hyapp.user.v1.UserHostService.ReviewAgencyApplication:input_type -> hyapp.user.v1.ReviewAgencyApplicationRequest
15, // 59: hyapp.user.v1.UserHostService.KickAgencyHost:input_type -> hyapp.user.v1.KickAgencyHostRequest
17, // 60: hyapp.user.v1.UserHostService.InviteAgency:input_type -> hyapp.user.v1.InviteAgencyRequest
19, // 61: hyapp.user.v1.UserHostService.InviteBD:input_type -> hyapp.user.v1.InviteBDRequest
21, // 62: hyapp.user.v1.UserHostService.ProcessRoleInvitation:input_type -> hyapp.user.v1.ProcessRoleInvitationRequest
23, // 63: hyapp.user.v1.UserHostService.GetHostProfile:input_type -> hyapp.user.v1.GetHostProfileRequest
25, // 64: hyapp.user.v1.UserHostService.GetBDProfile:input_type -> hyapp.user.v1.GetBDProfileRequest
27, // 65: hyapp.user.v1.UserHostService.GetCoinSellerProfile:input_type -> hyapp.user.v1.GetCoinSellerProfileRequest
29, // 66: hyapp.user.v1.UserHostService.ListActiveCoinSellersInMyRegion:input_type -> hyapp.user.v1.ListActiveCoinSellersInMyRegionRequest
31, // 67: hyapp.user.v1.UserHostService.GetUserRoleSummary:input_type -> hyapp.user.v1.GetUserRoleSummaryRequest
33, // 68: hyapp.user.v1.UserHostService.GetAgency:input_type -> hyapp.user.v1.GetAgencyRequest
35, // 69: hyapp.user.v1.UserHostService.CheckBusinessCapability:input_type -> hyapp.user.v1.CheckBusinessCapabilityRequest
37, // 70: hyapp.user.v1.UserHostService.GetAgencyMembers:input_type -> hyapp.user.v1.GetAgencyMembersRequest
39, // 71: hyapp.user.v1.UserHostService.GetAgencyApplications:input_type -> hyapp.user.v1.GetAgencyApplicationsRequest
41, // 72: hyapp.user.v1.UserHostAdminService.CreateBDLeader:input_type -> hyapp.user.v1.CreateBDLeaderRequest
43, // 73: hyapp.user.v1.UserHostAdminService.CreateBD:input_type -> hyapp.user.v1.CreateBDRequest
45, // 74: hyapp.user.v1.UserHostAdminService.SetBDStatus:input_type -> hyapp.user.v1.SetBDStatusRequest
47, // 75: hyapp.user.v1.UserHostAdminService.CreateCoinSeller:input_type -> hyapp.user.v1.CreateCoinSellerRequest
49, // 76: hyapp.user.v1.UserHostAdminService.SetCoinSellerStatus:input_type -> hyapp.user.v1.SetCoinSellerStatusRequest
51, // 77: hyapp.user.v1.UserHostAdminService.CreateAgency:input_type -> hyapp.user.v1.CreateAgencyRequest
53, // 78: hyapp.user.v1.UserHostAdminService.CloseAgency:input_type -> hyapp.user.v1.CloseAgencyRequest
55, // 79: hyapp.user.v1.UserHostAdminService.SetAgencyJoinEnabled:input_type -> hyapp.user.v1.SetAgencyJoinEnabledRequest
10, // 80: hyapp.user.v1.UserHostService.SearchAgencies:output_type -> hyapp.user.v1.SearchAgenciesResponse
12, // 81: hyapp.user.v1.UserHostService.ApplyToAgency:output_type -> hyapp.user.v1.ApplyToAgencyResponse
14, // 82: hyapp.user.v1.UserHostService.ReviewAgencyApplication:output_type -> hyapp.user.v1.ReviewAgencyApplicationResponse
16, // 83: hyapp.user.v1.UserHostService.KickAgencyHost:output_type -> hyapp.user.v1.KickAgencyHostResponse
18, // 84: hyapp.user.v1.UserHostService.InviteAgency:output_type -> hyapp.user.v1.InviteAgencyResponse
20, // 85: hyapp.user.v1.UserHostService.InviteBD:output_type -> hyapp.user.v1.InviteBDResponse
22, // 86: hyapp.user.v1.UserHostService.ProcessRoleInvitation:output_type -> hyapp.user.v1.ProcessRoleInvitationResponse
24, // 87: hyapp.user.v1.UserHostService.GetHostProfile:output_type -> hyapp.user.v1.GetHostProfileResponse
26, // 88: hyapp.user.v1.UserHostService.GetBDProfile:output_type -> hyapp.user.v1.GetBDProfileResponse
28, // 89: hyapp.user.v1.UserHostService.GetCoinSellerProfile:output_type -> hyapp.user.v1.GetCoinSellerProfileResponse
30, // 90: hyapp.user.v1.UserHostService.ListActiveCoinSellersInMyRegion:output_type -> hyapp.user.v1.ListActiveCoinSellersInMyRegionResponse
32, // 91: hyapp.user.v1.UserHostService.GetUserRoleSummary:output_type -> hyapp.user.v1.GetUserRoleSummaryResponse
34, // 92: hyapp.user.v1.UserHostService.GetAgency:output_type -> hyapp.user.v1.GetAgencyResponse
36, // 93: hyapp.user.v1.UserHostService.CheckBusinessCapability:output_type -> hyapp.user.v1.CheckBusinessCapabilityResponse
38, // 94: hyapp.user.v1.UserHostService.GetAgencyMembers:output_type -> hyapp.user.v1.GetAgencyMembersResponse
40, // 95: hyapp.user.v1.UserHostService.GetAgencyApplications:output_type -> hyapp.user.v1.GetAgencyApplicationsResponse
42, // 96: hyapp.user.v1.UserHostAdminService.CreateBDLeader:output_type -> hyapp.user.v1.CreateBDLeaderResponse
44, // 97: hyapp.user.v1.UserHostAdminService.CreateBD:output_type -> hyapp.user.v1.CreateBDResponse
46, // 98: hyapp.user.v1.UserHostAdminService.SetBDStatus:output_type -> hyapp.user.v1.SetBDStatusResponse
48, // 99: hyapp.user.v1.UserHostAdminService.CreateCoinSeller:output_type -> hyapp.user.v1.CreateCoinSellerResponse
50, // 100: hyapp.user.v1.UserHostAdminService.SetCoinSellerStatus:output_type -> hyapp.user.v1.SetCoinSellerStatusResponse
52, // 101: hyapp.user.v1.UserHostAdminService.CreateAgency:output_type -> hyapp.user.v1.CreateAgencyResponse
54, // 102: hyapp.user.v1.UserHostAdminService.CloseAgency:output_type -> hyapp.user.v1.CloseAgencyResponse
56, // 103: hyapp.user.v1.UserHostAdminService.SetAgencyJoinEnabled:output_type -> hyapp.user.v1.SetAgencyJoinEnabledResponse
80, // [80:104] is the sub-list for method output_type
56, // [56:80] is the sub-list for method input_type
56, // [56:56] is the sub-list for extension type_name
56, // [56:56] is the sub-list for extension extendee
0, // [0:56] is the sub-list for field type_name
}
func init() { file_proto_user_v1_host_proto_init() }
@ -4496,7 +4604,7 @@ func file_proto_user_v1_host_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_user_v1_host_proto_rawDesc), len(file_proto_user_v1_host_proto_rawDesc)),
NumEnums: 0,
NumMessages: 55,
NumMessages: 57,
NumExtensions: 0,
NumServices: 2,
},

View File

@ -283,6 +283,15 @@ message GetUserRoleSummaryResponse {
UserRoleSummary summary = 1;
}
message GetAgencyRequest {
RequestMeta meta = 1;
int64 agency_id = 2;
}
message GetAgencyResponse {
Agency agency = 1;
}
message CheckBusinessCapabilityRequest {
RequestMeta meta = 1;
int64 actor_user_id = 2;
@ -435,6 +444,7 @@ service UserHostService {
rpc GetCoinSellerProfile(GetCoinSellerProfileRequest) returns (GetCoinSellerProfileResponse);
rpc ListActiveCoinSellersInMyRegion(ListActiveCoinSellersInMyRegionRequest) returns (ListActiveCoinSellersInMyRegionResponse);
rpc GetUserRoleSummary(GetUserRoleSummaryRequest) returns (GetUserRoleSummaryResponse);
rpc GetAgency(GetAgencyRequest) returns (GetAgencyResponse);
rpc CheckBusinessCapability(CheckBusinessCapabilityRequest) returns (CheckBusinessCapabilityResponse);
rpc GetAgencyMembers(GetAgencyMembersRequest) returns (GetAgencyMembersResponse);
rpc GetAgencyApplications(GetAgencyApplicationsRequest) returns (GetAgencyApplicationsResponse);

View File

@ -31,6 +31,7 @@ const (
UserHostService_GetCoinSellerProfile_FullMethodName = "/hyapp.user.v1.UserHostService/GetCoinSellerProfile"
UserHostService_ListActiveCoinSellersInMyRegion_FullMethodName = "/hyapp.user.v1.UserHostService/ListActiveCoinSellersInMyRegion"
UserHostService_GetUserRoleSummary_FullMethodName = "/hyapp.user.v1.UserHostService/GetUserRoleSummary"
UserHostService_GetAgency_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgency"
UserHostService_CheckBusinessCapability_FullMethodName = "/hyapp.user.v1.UserHostService/CheckBusinessCapability"
UserHostService_GetAgencyMembers_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyMembers"
UserHostService_GetAgencyApplications_FullMethodName = "/hyapp.user.v1.UserHostService/GetAgencyApplications"
@ -54,6 +55,7 @@ type UserHostServiceClient interface {
GetCoinSellerProfile(ctx context.Context, in *GetCoinSellerProfileRequest, opts ...grpc.CallOption) (*GetCoinSellerProfileResponse, error)
ListActiveCoinSellersInMyRegion(ctx context.Context, in *ListActiveCoinSellersInMyRegionRequest, opts ...grpc.CallOption) (*ListActiveCoinSellersInMyRegionResponse, error)
GetUserRoleSummary(ctx context.Context, in *GetUserRoleSummaryRequest, opts ...grpc.CallOption) (*GetUserRoleSummaryResponse, error)
GetAgency(ctx context.Context, in *GetAgencyRequest, opts ...grpc.CallOption) (*GetAgencyResponse, 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)
@ -187,6 +189,16 @@ func (c *userHostServiceClient) GetUserRoleSummary(ctx context.Context, in *GetU
return out, nil
}
func (c *userHostServiceClient) GetAgency(ctx context.Context, in *GetAgencyRequest, opts ...grpc.CallOption) (*GetAgencyResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAgencyResponse)
err := c.cc.Invoke(ctx, UserHostService_GetAgency_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
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)
@ -235,6 +247,7 @@ type UserHostServiceServer interface {
GetCoinSellerProfile(context.Context, *GetCoinSellerProfileRequest) (*GetCoinSellerProfileResponse, error)
ListActiveCoinSellersInMyRegion(context.Context, *ListActiveCoinSellersInMyRegionRequest) (*ListActiveCoinSellersInMyRegionResponse, error)
GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error)
GetAgency(context.Context, *GetAgencyRequest) (*GetAgencyResponse, error)
CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error)
GetAgencyMembers(context.Context, *GetAgencyMembersRequest) (*GetAgencyMembersResponse, error)
GetAgencyApplications(context.Context, *GetAgencyApplicationsRequest) (*GetAgencyApplicationsResponse, error)
@ -284,6 +297,9 @@ func (UnimplementedUserHostServiceServer) ListActiveCoinSellersInMyRegion(contex
func (UnimplementedUserHostServiceServer) GetUserRoleSummary(context.Context, *GetUserRoleSummaryRequest) (*GetUserRoleSummaryResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetUserRoleSummary not implemented")
}
func (UnimplementedUserHostServiceServer) GetAgency(context.Context, *GetAgencyRequest) (*GetAgencyResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAgency not implemented")
}
func (UnimplementedUserHostServiceServer) CheckBusinessCapability(context.Context, *CheckBusinessCapabilityRequest) (*CheckBusinessCapabilityResponse, error) {
return nil, status.Error(codes.Unimplemented, "method CheckBusinessCapability not implemented")
}
@ -530,6 +546,24 @@ func _UserHostService_GetUserRoleSummary_Handler(srv interface{}, ctx context.Co
return interceptor(ctx, in, info, handler)
}
func _UserHostService_GetAgency_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAgencyRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserHostServiceServer).GetAgency(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserHostService_GetAgency_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserHostServiceServer).GetAgency(ctx, req.(*GetAgencyRequest))
}
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 {
@ -639,6 +673,10 @@ var UserHostService_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetUserRoleSummary",
Handler: _UserHostService_GetUserRoleSummary_Handler,
},
{
MethodName: "GetAgency",
Handler: _UserHostService_GetAgency_Handler,
},
{
MethodName: "CheckBusinessCapability",
Handler: _UserHostService_CheckBusinessCapability_Handler,

File diff suppressed because it is too large Load Diff

View File

@ -46,6 +46,43 @@ message DebitGiftResponse {
string host_period_cycle_key = 12;
}
// DebitGiftTarget
message DebitGiftTarget {
// command_id room-service command_id
string command_id = 1;
int64 target_user_id = 2;
bool target_is_host = 3;
int64 target_host_region_id = 4;
int64 target_agency_owner_user_id = 5;
}
// BatchDebitGiftRequest sender target
message BatchDebitGiftRequest {
string command_id = 1;
string room_id = 2;
int64 sender_user_id = 3;
string gift_id = 4;
int32 gift_count = 5;
string price_version = 6;
string app_code = 7;
int64 region_id = 8;
int64 sender_region_id = 9;
repeated DebitGiftTarget targets = 10;
}
// BatchDebitGiftReceipt
message BatchDebitGiftReceipt {
int64 target_user_id = 1;
string command_id = 2;
DebitGiftResponse billing = 3;
}
// BatchDebitGiftResponse
message BatchDebitGiftResponse {
DebitGiftResponse aggregate = 1;
repeated BatchDebitGiftReceipt receipts = 2;
}
// AssetBalance
message AssetBalance {
string asset_type = 1;
@ -609,6 +646,35 @@ message EquipUserResourceResponse {
UserResourceEntitlement resource = 1;
}
message UnequipUserResourceRequest {
string request_id = 1;
string app_code = 2;
int64 user_id = 3;
string resource_type = 4;
}
message UnequipUserResourceResponse {
string resource_type = 1;
bool unequipped = 2;
int64 updated_at_ms = 3;
}
message BatchGetUserEquippedResourcesRequest {
string request_id = 1;
string app_code = 2;
repeated int64 user_ids = 3;
repeated string resource_types = 4;
}
message UserEquippedResources {
int64 user_id = 1;
repeated UserResourceEntitlement resources = 2;
}
message BatchGetUserEquippedResourcesResponse {
repeated UserEquippedResources users = 1;
}
message ListResourceGrantsRequest {
string request_id = 1;
string app_code = 2;
@ -1396,6 +1462,7 @@ service WalletCronService {
// WalletService gRPC HTTP gateway-service
service WalletService {
rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse);
rpc BatchDebitGift(BatchDebitGiftRequest) returns (BatchDebitGiftResponse);
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
rpc AdminCreditAsset(AdminCreditAssetRequest) returns (AdminCreditAssetResponse);
rpc AdminCreditCoinSellerStock(AdminCreditCoinSellerStockRequest) returns (AdminCreditCoinSellerStockResponse);
@ -1420,6 +1487,8 @@ service WalletService {
rpc GrantResourceGroup(GrantResourceGroupRequest) returns (ResourceGrantResponse);
rpc ListUserResources(ListUserResourcesRequest) returns (ListUserResourcesResponse);
rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse);
rpc UnequipUserResource(UnequipUserResourceRequest) returns (UnequipUserResourceResponse);
rpc BatchGetUserEquippedResources(BatchGetUserEquippedResourcesRequest) returns (BatchGetUserEquippedResourcesResponse);
rpc ListResourceGrants(ListResourceGrantsRequest) returns (ListResourceGrantsResponse);
rpc ListResourceShopItems(ListResourceShopItemsRequest) returns (ListResourceShopItemsResponse);
rpc UpsertResourceShopItems(UpsertResourceShopItemsRequest) returns (UpsertResourceShopItemsResponse);

View File

@ -202,6 +202,7 @@ var WalletCronService_ServiceDesc = grpc.ServiceDesc{
const (
WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift"
WalletService_BatchDebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchDebitGift"
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
WalletService_AdminCreditAsset_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditAsset"
WalletService_AdminCreditCoinSellerStock_FullMethodName = "/hyapp.wallet.v1.WalletService/AdminCreditCoinSellerStock"
@ -226,6 +227,8 @@ const (
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
WalletService_UnequipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UnequipUserResource"
WalletService_BatchGetUserEquippedResources_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchGetUserEquippedResources"
WalletService_ListResourceGrants_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGrants"
WalletService_ListResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceShopItems"
WalletService_UpsertResourceShopItems_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertResourceShopItems"
@ -270,6 +273,7 @@ const (
// WalletService 是内部账务 gRPC 边界,外部 HTTP 入口仍由 gateway-service 承载。
type WalletServiceClient interface {
DebitGift(ctx context.Context, in *DebitGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error)
BatchDebitGift(ctx context.Context, in *BatchDebitGiftRequest, opts ...grpc.CallOption) (*BatchDebitGiftResponse, error)
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
AdminCreditAsset(ctx context.Context, in *AdminCreditAssetRequest, opts ...grpc.CallOption) (*AdminCreditAssetResponse, error)
AdminCreditCoinSellerStock(ctx context.Context, in *AdminCreditCoinSellerStockRequest, opts ...grpc.CallOption) (*AdminCreditCoinSellerStockResponse, error)
@ -294,6 +298,8 @@ type WalletServiceClient interface {
GrantResourceGroup(ctx context.Context, in *GrantResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error)
EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error)
UnequipUserResource(ctx context.Context, in *UnequipUserResourceRequest, opts ...grpc.CallOption) (*UnequipUserResourceResponse, error)
BatchGetUserEquippedResources(ctx context.Context, in *BatchGetUserEquippedResourcesRequest, opts ...grpc.CallOption) (*BatchGetUserEquippedResourcesResponse, error)
ListResourceGrants(ctx context.Context, in *ListResourceGrantsRequest, opts ...grpc.CallOption) (*ListResourceGrantsResponse, error)
ListResourceShopItems(ctx context.Context, in *ListResourceShopItemsRequest, opts ...grpc.CallOption) (*ListResourceShopItemsResponse, error)
UpsertResourceShopItems(ctx context.Context, in *UpsertResourceShopItemsRequest, opts ...grpc.CallOption) (*UpsertResourceShopItemsResponse, error)
@ -349,6 +355,16 @@ func (c *walletServiceClient) DebitGift(ctx context.Context, in *DebitGiftReques
return out, nil
}
func (c *walletServiceClient) BatchDebitGift(ctx context.Context, in *BatchDebitGiftRequest, opts ...grpc.CallOption) (*BatchDebitGiftResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchDebitGiftResponse)
err := c.cc.Invoke(ctx, WalletService_BatchDebitGift_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetBalancesResponse)
@ -589,6 +605,26 @@ func (c *walletServiceClient) EquipUserResource(ctx context.Context, in *EquipUs
return out, nil
}
func (c *walletServiceClient) UnequipUserResource(ctx context.Context, in *UnequipUserResourceRequest, opts ...grpc.CallOption) (*UnequipUserResourceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UnequipUserResourceResponse)
err := c.cc.Invoke(ctx, WalletService_UnequipUserResource_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) BatchGetUserEquippedResources(ctx context.Context, in *BatchGetUserEquippedResourcesRequest, opts ...grpc.CallOption) (*BatchGetUserEquippedResourcesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchGetUserEquippedResourcesResponse)
err := c.cc.Invoke(ctx, WalletService_BatchGetUserEquippedResources_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) ListResourceGrants(ctx context.Context, in *ListResourceGrantsRequest, opts ...grpc.CallOption) (*ListResourceGrantsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListResourceGrantsResponse)
@ -946,6 +982,7 @@ func (c *walletServiceClient) RetryRedPacketRefund(ctx context.Context, in *Retr
// WalletService 是内部账务 gRPC 边界,外部 HTTP 入口仍由 gateway-service 承载。
type WalletServiceServer interface {
DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error)
BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error)
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
AdminCreditAsset(context.Context, *AdminCreditAssetRequest) (*AdminCreditAssetResponse, error)
AdminCreditCoinSellerStock(context.Context, *AdminCreditCoinSellerStockRequest) (*AdminCreditCoinSellerStockResponse, error)
@ -970,6 +1007,8 @@ type WalletServiceServer interface {
GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error)
ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error)
EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error)
UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error)
BatchGetUserEquippedResources(context.Context, *BatchGetUserEquippedResourcesRequest) (*BatchGetUserEquippedResourcesResponse, error)
ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error)
ListResourceShopItems(context.Context, *ListResourceShopItemsRequest) (*ListResourceShopItemsResponse, error)
UpsertResourceShopItems(context.Context, *UpsertResourceShopItemsRequest) (*UpsertResourceShopItemsResponse, error)
@ -1018,6 +1057,9 @@ type UnimplementedWalletServiceServer struct{}
func (UnimplementedWalletServiceServer) DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error) {
return nil, status.Error(codes.Unimplemented, "method DebitGift not implemented")
}
func (UnimplementedWalletServiceServer) BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BatchDebitGift not implemented")
}
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
}
@ -1090,6 +1132,12 @@ func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *List
func (UnimplementedWalletServiceServer) EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method EquipUserResource not implemented")
}
func (UnimplementedWalletServiceServer) UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error) {
return nil, status.Error(codes.Unimplemented, "method UnequipUserResource not implemented")
}
func (UnimplementedWalletServiceServer) BatchGetUserEquippedResources(context.Context, *BatchGetUserEquippedResourcesRequest) (*BatchGetUserEquippedResourcesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method BatchGetUserEquippedResources not implemented")
}
func (UnimplementedWalletServiceServer) ListResourceGrants(context.Context, *ListResourceGrantsRequest) (*ListResourceGrantsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListResourceGrants not implemented")
}
@ -1234,6 +1282,24 @@ func _WalletService_DebitGift_Handler(srv interface{}, ctx context.Context, dec
return interceptor(ctx, in, info, handler)
}
func _WalletService_BatchDebitGift_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchDebitGiftRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).BatchDebitGift(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_BatchDebitGift_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).BatchDebitGift(ctx, req.(*BatchDebitGiftRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_GetBalances_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetBalancesRequest)
if err := dec(in); err != nil {
@ -1666,6 +1732,42 @@ func _WalletService_EquipUserResource_Handler(srv interface{}, ctx context.Conte
return interceptor(ctx, in, info, handler)
}
func _WalletService_UnequipUserResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UnequipUserResourceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).UnequipUserResource(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_UnequipUserResource_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).UnequipUserResource(ctx, req.(*UnequipUserResourceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_BatchGetUserEquippedResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchGetUserEquippedResourcesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).BatchGetUserEquippedResources(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_BatchGetUserEquippedResources_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).BatchGetUserEquippedResources(ctx, req.(*BatchGetUserEquippedResourcesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_ListResourceGrants_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListResourceGrantsRequest)
if err := dec(in); err != nil {
@ -2307,6 +2409,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "DebitGift",
Handler: _WalletService_DebitGift_Handler,
},
{
MethodName: "BatchDebitGift",
Handler: _WalletService_BatchDebitGift_Handler,
},
{
MethodName: "GetBalances",
Handler: _WalletService_GetBalances_Handler,
@ -2403,6 +2509,14 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "EquipUserResource",
Handler: _WalletService_EquipUserResource_Handler,
},
{
MethodName: "UnequipUserResource",
Handler: _WalletService_UnequipUserResource_Handler,
},
{
MethodName: "BatchGetUserEquippedResources",
Handler: _WalletService_BatchGetUserEquippedResources_Handler,
},
{
MethodName: "ListResourceGrants",
Handler: _WalletService_ListResourceGrants_Handler,

View File

@ -0,0 +1,475 @@
# 用户装扮 Flutter 对接文档
## 通用说明
所有需要登录的接口都需要带:
```http
Authorization: Bearer <token>
```
统一返回外壳:
```json
{
"code": "OK",
"message": "OK",
"request_id": "xxx",
"data": {}
}
```
下文只描述 `data` 内部字段。
## 1. 获取我的装扮聚合
地址:
```http
GET /api/v1/appearance
GET /api/v1/users/me/appearance
```
参数:无。
返回值:
```json
{
"user_id": "42",
"avatar_frame": {
"resource_id": 8101,
"resource_code": "frame_gold",
"resource_type": "avatar_frame",
"name": "Gold Frame",
"asset_url": "https://cdn.example/frame.png",
"preview_url": "",
"animation_url": "https://cdn.example/frame.svga",
"metadata_json": "{}",
"entitlement_id": "ent-frame",
"expires_at_ms": 1999999999999
},
"profile_card": {},
"vehicle": {
"resource_id": 8102,
"resource_code": "vehicle_gold",
"resource_type": "vehicle",
"name": "Gold Vehicle",
"asset_url": "https://cdn.example/vehicle.png",
"preview_url": "",
"animation_url": "https://cdn.example/vehicle.svga",
"metadata_json": "{}",
"entitlement_id": "ent-vehicle",
"expires_at_ms": 1999999999999
},
"equipped_badges": [],
"badges": [
{
"slot": "strip",
"position": 1,
"badge_form": "icon",
"resource_id": 8103,
"resource": {
"resource_id": 8103,
"resource_code": "badge_host",
"resource_type": "badge",
"name": "Host Badge",
"asset_url": "https://cdn.example/badge.png",
"preview_url": "",
"animation_url": "",
"metadata_json": "{}",
"entitlement_id": "",
"expires_at_ms": 0
},
"entitlement_id": "ent-badge",
"source_type": "achievement",
"source_id": "ach-host",
"pin_mode": "auto",
"updated_at_ms": 1778256000000
}
],
"badge_profile": {
"strip_badges": [],
"profile_tile_badges": [],
"honor_badges": [],
"server_time_ms": 1778256000000
},
"resources": [],
"server_time_ms": 1778256000000
}
```
字段说明:
| 字段 | 说明 |
|---|---|
| user_id | 用户 ID字符串 |
| avatar_frame | 当前佩戴头像框,未佩戴时为 `{}` |
| profile_card | 当前佩戴背景卡,未佩戴时为 `{}` |
| vehicle | 当前佩戴入场座驾,未佩戴时为 `{}` |
| equipped_badges | wallet 当前佩戴的 badge 权益 |
| badges | 房间、公屏等高频展示用徽章,取 `strip_badges` |
| badge_profile | 完整徽章展示配置 |
| resources | 当前佩戴资源的完整列表 |
| server_time_ms | 服务端时间戳 |
说明:
- 过期佩戴不会返回。
- 头像框、背景卡、入场座驾来自 wallet 当前佩戴。
- 徽章展示配置来自 activity。
- 客户端渲染动画优先使用 `animation_url`,没有动画时用 `asset_url``preview_url` 兜底。
## 2. 获取我的资源背包
地址:
```http
GET /api/v1/users/me/resources
```
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| resource_type | string | 否 | `avatar_frame` / `profile_card` / `vehicle` / `badge` / `chat_bubble` |
示例:
```http
GET /api/v1/users/me/resources?resource_type=vehicle
```
返回值:
```json
{
"items": [
{
"entitlement_id": "ent-vehicle",
"resource_id": 8102,
"resource": {
"resource_id": 8102,
"resource_code": "vehicle_gold",
"resource_type": "vehicle",
"name": "Gold Vehicle",
"status": "active",
"usage_scopes": ["room_entry"],
"asset_url": "https://cdn.example/vehicle.png",
"preview_url": "",
"animation_url": "https://cdn.example/vehicle.svga",
"metadata_json": "{}"
},
"status": "active",
"quantity": 1,
"remaining_quantity": 1,
"effective_at_ms": 1778256000000,
"expires_at_ms": 1999999999999,
"source_grant_id": "grant-1",
"created_at_ms": 1778256000000,
"updated_at_ms": 1778256000000,
"equipped": true
}
],
"total": 1
}
```
字段说明:
| 字段 | 说明 |
|---|---|
| entitlement_id | 用户权益 ID佩戴时建议传这个值 |
| resource_id | 资源 ID |
| resource | 资源素材和基础信息 |
| status | 权益状态 |
| quantity | 总数量 |
| remaining_quantity | 剩余数量 |
| effective_at_ms | 生效时间 |
| expires_at_ms | 过期时间0 表示不过期 |
| equipped | 是否当前佩戴 |
## 3. 佩戴资源
地址:
```http
POST /api/v1/users/me/resources/{resource_id}/equip
```
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| resource_id | int64 | 是 | 资源 ID |
Body
```json
{
"entitlement_id": "ent-vehicle"
}
```
Body 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| entitlement_id | string | 否 | 用户权益 ID建议传 |
返回值:
```json
{
"entitlement_id": "ent-vehicle",
"resource_id": 8102,
"resource": {},
"status": "active",
"quantity": 1,
"remaining_quantity": 1,
"effective_at_ms": 1778256000000,
"expires_at_ms": 1999999999999,
"equipped": true
}
```
说明:
- 可佩戴类型:`avatar_frame``profile_card``vehicle``chat_bubble``badge`
- 过期资源不能佩戴。
- 不传 `entitlement_id` 时,服务端会选择当前有效权益。
## 4. 取消佩戴
地址:
```http
DELETE /api/v1/users/me/resources/{resource_type}/unequip
```
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| resource_type | string | 是 | `avatar_frame` / `profile_card` / `vehicle` / `chat_bubble` / `badge` |
返回值:
```json
{
"resource_type": "vehicle",
"unequipped": true,
"updated_at_ms": 1778256000000
}
```
说明:
- 重复取消不会报错。
- 用户本来没佩戴该类型时,`unequipped=false`
## 5. 批量获取房间展示资料
地址:
```http
GET /api/v1/users/room-display-profiles:batch
```
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| user_ids | string | 是 | 多个用户 ID逗号分隔 |
| user_id | string | 否 | 也支持重复传多个 `user_id` |
示例:
```http
GET /api/v1/users/room-display-profiles:batch?user_ids=42,43
```
返回值:
```json
{
"profiles": [
{
"user_id": "42",
"username": "Tom",
"avatar": "https://cdn.example/avatar.png",
"display_user_id": "100001",
"gender": "male",
"age": 22,
"country": "CN",
"country_name": "China",
"country_display_name": "China",
"country_flag": "CN",
"vip": {},
"level": {},
"badges": [],
"avatar_frame": {
"resource_id": 8101,
"resource_code": "frame_gold",
"resource_type": "avatar_frame",
"name": "Gold Frame",
"asset_url": "https://cdn.example/frame.png",
"preview_url": "",
"animation_url": "https://cdn.example/frame.svga",
"metadata_json": "{}",
"entitlement_id": "ent-frame",
"expires_at_ms": 1999999999999
},
"vehicle": {
"resource_id": 8102,
"resource_code": "vehicle_gold",
"resource_type": "vehicle",
"name": "Gold Vehicle",
"asset_url": "https://cdn.example/vehicle.png",
"preview_url": "",
"animation_url": "https://cdn.example/vehicle.svga",
"metadata_json": "{}",
"entitlement_id": "ent-vehicle",
"expires_at_ms": 1999999999999
},
"charm": 0
}
]
}
```
说明:
- 房间、公屏、在线列表展示头像框用 `avatar_frame`
- 入场座驾展示用 `vehicle`
- 徽章条展示用 `badges`
- 过期佩戴不会返回。
## 6. 进房
地址:
```http
POST /api/v1/rooms/join
```
Body
```json
{
"room_id": "room_1001",
"command_id": "cmd-join-1001",
"role": "audience",
"password": "1234"
}
```
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| room_id | string | 是 | 房间 ID |
| command_id | string | 是 | 幂等 IDFlutter 每次进房动作生成一个 |
| role | string | 否 | 默认 `audience` |
| password | string | 否 | 锁房密码 |
返回值:
```json
{
"result": {
"applied": true,
"room_version": 9,
"server_time_ms": 1778256000000
},
"room": {},
"viewer": {},
"seats": [],
"contribution_rank": [],
"profiles": [
{
"user_id": "42",
"display_user_id": "100001",
"username": "Tom",
"avatar": "https://cdn.example/avatar.png"
}
],
"im": {
"group_id": "room_1001",
"need_join_group": true
},
"rtc": {
"need_token": true,
"available": true,
"token": {}
},
"server_time_ms": 1778256000000
}
```
说明:
- Flutter 不需要传入场座驾。
- 服务端会在进房时自动读取当前佩戴的 `vehicle`,写入房间 IM。
- `join` 返回里的 `profiles` 是基础资料。
- 房间增强资料用 `/users/room-display-profiles:batch`,或使用房间详情、在线用户列表里内嵌的 `profile` 字段。
## 7. 相关 IM
### 用户进房
腾讯 IM 群自定义消息:
```json
{
"event_id": "evt_xxx",
"room_id": "room_1001",
"event_type": "room_user_joined",
"actor_user_id": 42,
"target_user_id": 42,
"room_version": 9,
"entry_vehicle": {
"resource_id": 8102,
"resource_code": "vehicle_gold",
"name": "Gold Vehicle",
"asset_url": "https://cdn.example/vehicle.png",
"preview_url": "",
"animation_url": "https://cdn.example/vehicle.svga",
"metadata_json": "{}",
"entitlement_id": "ent-vehicle",
"expires_at_ms": 1999999999999
},
"attributes": {
"role": "audience"
}
}
```
Flutter 处理:
| 字段 | 说明 |
|---|---|
| event_type | `room_user_joined` 表示用户进入房间 |
| actor_user_id | 进房用户 ID |
| target_user_id | 进房用户 ID |
| room_version | 房间版本 |
| entry_vehicle | 入场座驾快照 |
| attributes.role | 用户进房角色,仅用于展示 |
处理规则:
- `entry_vehicle.resource_id > 0`:播放入场座驾。
- `entry_vehicle.animation_url`:优先用于动画播放。
- `entry_vehicle.asset_url` / `entry_vehicle.preview_url`:动画不可用时兜底展示。
- `entry_vehicle` 不存在或为空:普通进房,不播放座驾。
- 不要收到进房 IM 后再实时请求 `/appearance` 查询座驾,直接使用 IM 中的 `entry_vehicle` 快照。
## 8. Flutter 调用建议
1. 个人装扮页进入时调 `/api/v1/appearance`
2. 背包页调 `/api/v1/users/me/resources?resource_type=xxx`
3. 点击佩戴调 `POST /api/v1/users/me/resources/{resource_id}/equip`,成功后刷新 `/appearance`
4. 点击取消佩戴调 `DELETE /api/v1/users/me/resources/{resource_type}/unequip`,成功后刷新 `/appearance`
5. 房间、公屏、在线用户资料优先使用 `room-display-profiles:batch``avatar_frame``vehicle``badges`
6. 收到 `room_user_joined` IM 时,只按 `entry_vehicle` 播放入场座驾。

View File

@ -104,9 +104,25 @@ type RoomEvent struct {
RoomHeat int64 `json:"room_heat,omitempty"`
RoomVersion int64 `json:"room_version,omitempty"`
Text string `json:"text,omitempty"`
// EntryVehicle 是用户进房瞬间的座驾快照;客户端收到 room_user_joined 后直接播放,不再反查 wallet。
EntryVehicle *RoomEntryVehicleSnapshot `json:"entry_vehicle,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
}
// RoomEntryVehicleSnapshot 是房间系统消息里可直接渲染的座驾素材快照。
// 它故意不包含 wallet 内部状态,只保留客户端展示和过期兜底需要的字段。
type RoomEntryVehicleSnapshot struct {
ResourceID int64 `json:"resource_id"`
ResourceCode string `json:"resource_code,omitempty"`
Name string `json:"name,omitempty"`
AssetURL string `json:"asset_url,omitempty"`
PreviewURL string `json:"preview_url,omitempty"`
AnimationURL string `json:"animation_url,omitempty"`
MetadataJSON string `json:"metadata_json,omitempty"`
EntitlementID string `json:"entitlement_id,omitempty"`
ExpiresAtMS int64 `json:"expires_at_ms,omitempty"`
}
// RESTClient 封装腾讯云 IM REST API把 UserSig、endpoint query 和错误码处理收敛在一个边界。
type RESTClient struct {
// cfg 初始化后不再修改,因此同一个 client 可以被多个 worker 并发复用。

View File

@ -433,10 +433,15 @@ func (h *Handler) GrantResource(c *gin.Context) {
response.BadRequest(c, "资源赠送参数不正确")
return
}
targetUserID, err := parseGrantTargetUserID(req.TargetUserID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
resp, err := h.wallet.GrantResource(c.Request.Context(), &walletv1.GrantResourceRequest{
CommandId: strings.TrimSpace(req.CommandID),
AppCode: appctx.FromContext(c.Request.Context()),
TargetUserId: req.TargetUserID,
TargetUserId: targetUserID,
ResourceId: req.ResourceID,
Quantity: req.Quantity,
DurationMs: req.DurationMS,
@ -459,10 +464,15 @@ func (h *Handler) GrantResourceGroup(c *gin.Context) {
response.BadRequest(c, "资源组赠送参数不正确")
return
}
targetUserID, err := parseGrantTargetUserID(req.TargetUserID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
resp, err := h.wallet.GrantResourceGroup(c.Request.Context(), &walletv1.GrantResourceGroupRequest{
CommandId: strings.TrimSpace(req.CommandID),
AppCode: appctx.FromContext(c.Request.Context()),
TargetUserId: req.TargetUserID,
TargetUserId: targetUserID,
GroupId: req.GroupID,
Reason: strings.TrimSpace(req.Reason),
OperatorUserId: actorID(c),
@ -477,6 +487,45 @@ func (h *Handler) GrantResourceGroup(c *gin.Context) {
response.Created(c, grant)
}
func (h *Handler) LookupResourceGrantTarget(c *gin.Context) {
keyword := strings.TrimSpace(firstQuery(c, "user_id", "userId", "keyword"))
if keyword == "" {
response.BadRequest(c, "请输入用户 ID 或短号")
return
}
if h == nil || h.userDB == nil {
response.ServerError(c, "用户数据库未配置")
return
}
// 资源赠送页允许输入短号,但 wallet 只认内部 user_id这里在 admin 侧先解析,
// 避免前端把短号直接发给 wallet也避免 JS number 对长 user_id 造成精度损失。
row := h.userDB.QueryRowContext(c.Request.Context(), `
SELECT user_id, current_display_user_id, COALESCE(username, ''), COALESCE(avatar, '')
FROM users
WHERE app_code = ?
AND (CAST(user_id AS CHAR) = ? OR current_display_user_id = ?)
ORDER BY
CASE
WHEN CAST(user_id AS CHAR) = ? THEN 0
WHEN current_display_user_id = ? THEN 1
ELSE 2
END,
user_id DESC
LIMIT 1`,
appctx.FromContext(c.Request.Context()), keyword, keyword, keyword, keyword,
)
user := grantUserDTO{}
if err := row.Scan(&user.UserID, &user.DisplayUserID, &user.Username, &user.Avatar); err != nil {
if err == sql.ErrNoRows {
response.BadRequest(c, "用户不存在")
return
}
response.ServerError(c, "查询用户失败")
return
}
response.OK(c, user)
}
func (h *Handler) ListResourceGrants(c *gin.Context) {
options := shared.ListOptions(c)
targetUserID, ok := optionalInt64Query(c, "target_user_id", "targetUserId")
@ -685,3 +734,13 @@ func optionalInt64QueryWithPresence(c *gin.Context, primary string, fallback str
}
return value, true, true
}
func firstQuery(c *gin.Context, keys ...string) string {
for _, key := range keys {
value := strings.TrimSpace(c.Query(key))
if value != "" {
return value
}
}
return ""
}

View File

@ -5,6 +5,26 @@ import (
"testing"
)
func TestParseGrantTargetUserIDAcceptsStringLongID(t *testing.T) {
userID, err := parseGrantTargetUserID("318705991371722752")
if err != nil {
t.Fatalf("string long user id should be accepted: %v", err)
}
if userID != 318705991371722752 {
t.Fatalf("user id mismatch: %d", userID)
}
}
func TestParseGrantTargetUserIDKeepsLegacyNumber(t *testing.T) {
userID, err := parseGrantTargetUserID(float64(163000))
if err != nil {
t.Fatalf("legacy numeric user id should be accepted: %v", err)
}
if userID != 163000 {
t.Fatalf("user id mismatch: %d", userID)
}
}
func TestApplyGrantOperatorsUsesSourceSpecificProfiles(t *testing.T) {
grants := []grantDTO{
{GrantID: "grant-admin", GrantSource: "admin", OperatorUserID: 7},

View File

@ -3,6 +3,7 @@ package resource
import (
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"unicode/utf8"
@ -136,7 +137,7 @@ type giftTypeItemRequest struct {
type grantResourceRequest struct {
CommandID string `json:"commandId"`
TargetUserID int64 `json:"targetUserId"`
TargetUserID any `json:"targetUserId"`
ResourceID int64 `json:"resourceId"`
Quantity int64 `json:"quantity"`
DurationMS int64 `json:"durationMs"`
@ -145,7 +146,7 @@ type grantResourceRequest struct {
type grantGroupRequest struct {
CommandID string `json:"commandId"`
TargetUserID int64 `json:"targetUserId"`
TargetUserID any `json:"targetUserId"`
GroupID int64 `json:"groupId"`
Reason string `json:"reason"`
}
@ -194,6 +195,45 @@ func (r resourceRequest) createProto(c *gin.Context) *walletv1.CreateResourceReq
}
}
func parseGrantTargetUserID(value any) (int64, error) {
// Admin 前端会先把短号解析成内部 user_id内部 user_id 已超过 JS 安全整数,
// 所以新请求用字符串传递,避免浏览器把 318705991371722752 这类 ID 四舍五入。
switch typed := value.(type) {
case string:
userID, err := strconv.ParseInt(strings.TrimSpace(typed), 10, 64)
if err != nil || userID <= 0 {
return 0, fmt.Errorf("target_user_id is invalid")
}
return userID, nil
case json.Number:
userID, err := typed.Int64()
if err != nil || userID <= 0 {
return 0, fmt.Errorf("target_user_id is invalid")
}
return userID, nil
case float64:
// 兼容旧 admin-platform 对短号或小整数 user_id 传 JSON number 的请求;
// 长 ID 必须走字符串,否则到这里前精度已经不可恢复。
userID := int64(typed)
if float64(userID) != typed || userID <= 0 {
return 0, fmt.Errorf("target_user_id is invalid")
}
return userID, nil
case int64:
if typed <= 0 {
return 0, fmt.Errorf("target_user_id is invalid")
}
return typed, nil
case int:
if typed <= 0 {
return 0, fmt.Errorf("target_user_id is invalid")
}
return int64(typed), nil
default:
return 0, fmt.Errorf("target_user_id is required")
}
}
func (r resourceRequest) updateProto(c *gin.Context, resourceID int64) *walletv1.UpdateResourceRequest {
resourceType := normalizeResourceType(r.ResourceType)
walletAssetType, walletAssetAmount := resourceWalletAsset(resourceType, r.Amount)

View File

@ -41,6 +41,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.POST("/admin/resource-grants/resource", middleware.RequirePermission("resource-grant:create"), h.GrantResource)
protected.POST("/admin/resource-grants/group", middleware.RequirePermission("resource-grant:create"), h.GrantResourceGroup)
protected.GET("/admin/resource-grants/target", middleware.RequirePermission("resource-grant:create"), h.LookupResourceGrantTarget)
protected.GET("/admin/resource-grants", middleware.RequirePermission("resource-grant:view"), h.ListResourceGrants)
protected.GET("/admin/resource-shop/items", middleware.RequirePermission("resource-shop:view"), h.ListResourceShopItems)

View File

@ -74,7 +74,12 @@ type UserHostClient interface {
GetCoinSellerProfile(ctx context.Context, req *userv1.GetCoinSellerProfileRequest) (*userv1.GetCoinSellerProfileResponse, error)
ListActiveCoinSellersInMyRegion(ctx context.Context, req *userv1.ListActiveCoinSellersInMyRegionRequest) (*userv1.ListActiveCoinSellersInMyRegionResponse, error)
GetUserRoleSummary(ctx context.Context, req *userv1.GetUserRoleSummaryRequest) (*userv1.GetUserRoleSummaryResponse, error)
GetAgency(ctx context.Context, req *userv1.GetAgencyRequest) (*userv1.GetAgencyResponse, error)
CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error)
GetAgencyMembers(ctx context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error)
GetAgencyApplications(ctx context.Context, req *userv1.GetAgencyApplicationsRequest) (*userv1.GetAgencyApplicationsResponse, error)
ReviewAgencyApplication(ctx context.Context, req *userv1.ReviewAgencyApplicationRequest) (*userv1.ReviewAgencyApplicationResponse, error)
KickAgencyHost(ctx context.Context, req *userv1.KickAgencyHostRequest) (*userv1.KickAgencyHostResponse, error)
}
// AppRegistryClient 抽象 gateway 对 user-service App 注册表的解析依赖。
@ -330,10 +335,30 @@ func (c *grpcUserHostClient) GetUserRoleSummary(ctx context.Context, req *userv1
return c.client.GetUserRoleSummary(ctx, req)
}
func (c *grpcUserHostClient) GetAgency(ctx context.Context, req *userv1.GetAgencyRequest) (*userv1.GetAgencyResponse, error) {
return c.client.GetAgency(ctx, req)
}
func (c *grpcUserHostClient) CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error) {
return c.client.CheckBusinessCapability(ctx, req)
}
func (c *grpcUserHostClient) GetAgencyMembers(ctx context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error) {
return c.client.GetAgencyMembers(ctx, req)
}
func (c *grpcUserHostClient) GetAgencyApplications(ctx context.Context, req *userv1.GetAgencyApplicationsRequest) (*userv1.GetAgencyApplicationsResponse, error) {
return c.client.GetAgencyApplications(ctx, req)
}
func (c *grpcUserHostClient) ReviewAgencyApplication(ctx context.Context, req *userv1.ReviewAgencyApplicationRequest) (*userv1.ReviewAgencyApplicationResponse, error) {
return c.client.ReviewAgencyApplication(ctx, req)
}
func (c *grpcUserHostClient) KickAgencyHost(ctx context.Context, req *userv1.KickAgencyHostRequest) (*userv1.KickAgencyHostResponse, error) {
return c.client.KickAgencyHost(ctx, req)
}
func (c *grpcAppRegistryClient) ResolveApp(ctx context.Context, req *userv1.ResolveAppRequest) (*userv1.ResolveAppResponse, error) {
return c.client.ResolveApp(ctx, req)
}

View File

@ -33,6 +33,8 @@ type WalletClient interface {
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)
UnequipUserResource(ctx context.Context, req *walletv1.UnequipUserResourceRequest) (*walletv1.UnequipUserResourceResponse, error)
BatchGetUserEquippedResources(ctx context.Context, req *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error)
GetRedPacketConfig(ctx context.Context, req *walletv1.GetRedPacketConfigRequest) (*walletv1.GetRedPacketConfigResponse, error)
CreateRedPacket(ctx context.Context, req *walletv1.CreateRedPacketRequest) (*walletv1.CreateRedPacketResponse, error)
ClaimRedPacket(ctx context.Context, req *walletv1.ClaimRedPacketRequest) (*walletv1.ClaimRedPacketResponse, error)
@ -141,6 +143,14 @@ func (c *grpcWalletClient) EquipUserResource(ctx context.Context, req *walletv1.
return c.client.EquipUserResource(ctx, req)
}
func (c *grpcWalletClient) UnequipUserResource(ctx context.Context, req *walletv1.UnequipUserResourceRequest) (*walletv1.UnequipUserResourceResponse, error) {
return c.client.UnequipUserResource(ctx, req)
}
func (c *grpcWalletClient) BatchGetUserEquippedResources(ctx context.Context, req *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error) {
return c.client.BatchGetUserEquippedResources(ctx, req)
}
func (c *grpcWalletClient) GetRedPacketConfig(ctx context.Context, req *walletv1.GetRedPacketConfigRequest) (*walletv1.GetRedPacketConfigResponse, error) {
return c.client.GetRedPacketConfig(ctx, req)
}

View File

@ -72,6 +72,11 @@ type UserHandlers struct {
GetMyRoleSummary http.HandlerFunc
SearchHostAgencies http.HandlerFunc
ApplyToHostAgency http.HandlerFunc
GetAgencyCenterOverview http.HandlerFunc
ListAgencyCenterHosts http.HandlerFunc
ListAgencyCenterApplications http.HandlerFunc
ReviewAgencyCenterApplication http.HandlerFunc
RemoveAgencyCenterHost http.HandlerFunc
CompleteMyOnboarding http.HandlerFunc
UpdateMyProfile http.HandlerFunc
ChangeMyCountry http.HandlerFunc
@ -82,8 +87,11 @@ type UserHandlers struct {
ListMyFriends http.HandlerFunc
ListMyFriendApplications http.HandlerFunc
GetMyProfile http.HandlerFunc
GetMyAppearance http.HandlerFunc
GetUserAppearance http.HandlerFunc
ListMyResources http.HandlerFunc
EquipMyResource http.HandlerFunc
UnequipMyResource http.HandlerFunc
UserSocialAction http.HandlerFunc
SubmitReport http.HandlerFunc
}
@ -293,6 +301,11 @@ func (r routes) registerUserRoutes() {
r.profile("/users/me/role-summary", "", h.GetMyRoleSummary)
r.profile("/host/agencies/search", http.MethodGet, h.SearchHostAgencies)
r.profile("/host/agency-applications", http.MethodPost, h.ApplyToHostAgency)
r.profile("/agency-center/overview", http.MethodGet, h.GetAgencyCenterOverview)
r.profile("/agency-center/hosts", http.MethodGet, h.ListAgencyCenterHosts)
r.profile("/agency-center/applications", http.MethodGet, h.ListAgencyCenterApplications)
r.profile("/agency-center/applications/{application_id}/review", http.MethodPost, h.ReviewAgencyCenterApplication)
r.profile("/agency-center/hosts/{host_user_id}/remove", http.MethodPost, h.RemoveAgencyCenterHost)
r.auth("/users/me/onboarding/complete", "", h.CompleteMyOnboarding)
r.profile("/users/me/profile/update", "", h.UpdateMyProfile)
r.profile("/users/me/country/change", "", h.ChangeMyCountry)
@ -302,9 +315,12 @@ func (r routes) registerUserRoutes() {
r.profile("/users/me/following", http.MethodGet, h.ListMyFollowing)
r.profile("/users/me/friends", http.MethodGet, h.ListMyFriends)
r.profile("/users/me/friend-requests", http.MethodGet, h.ListMyFriendApplications)
r.profile("/appearance", http.MethodGet, h.GetMyAppearance)
r.profile("/users/me/appearance", http.MethodGet, h.GetMyAppearance)
r.profile("/users/me", "", h.GetMyProfile)
r.profile("/users/me/resources", "", h.ListMyResources)
r.profile("/users/me/resources/{resource_id}/equip", "", h.EquipMyResource)
r.profile("/users/me/resources/{resource_type}/unequip", http.MethodDelete, h.UnequipMyResource)
r.profile("/reports", http.MethodPost, h.SubmitReport)
r.profile("/users/{user_id}/{action...}", "", h.UserSocialAction)
}

View File

@ -55,3 +55,7 @@ func (h *Handler) ListMyResources(writer http.ResponseWriter, request *http.Requ
func (h *Handler) EquipMyResource(writer http.ResponseWriter, request *http.Request) {
h.equipMyResource(writer, request)
}
func (h *Handler) UnequipMyResource(writer http.ResponseWriter, request *http.Request) {
h.unequipMyResource(writer, request)
}

View File

@ -498,6 +498,37 @@ func (h *Handler) equipMyResource(writer http.ResponseWriter, request *http.Requ
httpkit.WriteOK(writer, request, userResourceFromProto(resp.GetResource()))
}
func (h *Handler) unequipMyResource(writer http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodDelete {
httpkit.WriteError(writer, request, http.StatusMethodNotAllowed, httpkit.CodeInvalidArgument, "invalid argument")
return
}
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
resourceType := strings.TrimSpace(request.PathValue("resource_type"))
if resourceType == "" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.walletClient.UnequipUserResource(request.Context(), &walletv1.UnequipUserResourceRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserId: auth.UserIDFromContext(request.Context()),
ResourceType: resourceType,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{
"resource_type": resp.GetResourceType(),
"unequipped": resp.GetUnequipped(),
"updated_at_ms": resp.GetUpdatedAtMs(),
})
}
func resourceFromProto(item *walletv1.Resource) resourceData {
if item == nil {
return resourceData{}

View File

@ -404,6 +404,21 @@ type fakeUserHostClient struct {
lastRoleSummary *userv1.GetUserRoleSummaryRequest
roleSummary *userv1.UserRoleSummary
roleSummaryErr error
lastGetAgency *userv1.GetAgencyRequest
getAgencyResp *userv1.GetAgencyResponse
getAgencyErr error
lastMembers *userv1.GetAgencyMembersRequest
membersResp *userv1.GetAgencyMembersResponse
membersErr error
lastApplications *userv1.GetAgencyApplicationsRequest
applicationsResp *userv1.GetAgencyApplicationsResponse
applicationsErr error
lastReview *userv1.ReviewAgencyApplicationRequest
reviewResp *userv1.ReviewAgencyApplicationResponse
reviewErr error
lastKick *userv1.KickAgencyHostRequest
kickResp *userv1.KickAgencyHostResponse
kickErr error
lastCapability *userv1.CheckBusinessCapabilityRequest
capabilityResp *userv1.CheckBusinessCapabilityResponse
capabilityErr error
@ -411,7 +426,9 @@ type fakeUserHostClient struct {
type fakeWalletClient struct {
last *walletv1.GetBalancesRequest
balanceRequests []*walletv1.GetBalancesRequest
resp *walletv1.GetBalancesResponse
balancesByUserID map[int64]*walletv1.GetBalancesResponse
err error
lastOverview *walletv1.GetWalletOverviewRequest
overviewResp *walletv1.GetWalletOverviewResponse
@ -458,6 +475,8 @@ type fakeWalletClient struct {
lastGrantResource *walletv1.GrantResourceRequest
grantResourceResp *walletv1.ResourceGrantResponse
grantResourceErr error
lastBatchEquipped *walletv1.BatchGetUserEquippedResourcesRequest
batchEquippedResp *walletv1.BatchGetUserEquippedResourcesResponse
lastListRedPackets *walletv1.ListRedPacketsRequest
listRedPacketsResp *walletv1.ListRedPacketsResponse
lastGetRedPacket *walletv1.GetRedPacketRequest
@ -640,6 +659,12 @@ func (f *fakeUserProfileClient) BatchGetUsers(_ context.Context, req *userv1.Bat
f.lastBatch = req
users := make(map[int64]*userv1.User, len(req.GetUserIds()))
for _, userID := range req.GetUserIds() {
if f.usersByID != nil {
if user, ok := f.usersByID[userID]; ok {
users[userID] = user
continue
}
}
users[userID] = &userv1.User{
UserId: userID,
DisplayUserId: strconv.FormatInt(100000+userID, 10),
@ -1104,6 +1129,17 @@ func (f *fakeUserHostClient) GetUserRoleSummary(_ context.Context, req *userv1.G
return &userv1.GetUserRoleSummaryResponse{Summary: &userv1.UserRoleSummary{UserId: req.GetUserId()}}, nil
}
func (f *fakeUserHostClient) GetAgency(_ context.Context, req *userv1.GetAgencyRequest) (*userv1.GetAgencyResponse, error) {
f.lastGetAgency = req
if f.getAgencyErr != nil {
return nil, f.getAgencyErr
}
if f.getAgencyResp != nil {
return f.getAgencyResp, nil
}
return &userv1.GetAgencyResponse{}, nil
}
func (f *fakeUserHostClient) CheckBusinessCapability(_ context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error) {
f.lastCapability = req
if f.capabilityErr != nil {
@ -1115,11 +1151,61 @@ func (f *fakeUserHostClient) CheckBusinessCapability(_ context.Context, req *use
return &userv1.CheckBusinessCapabilityResponse{Allowed: true}, nil
}
func (f *fakeUserHostClient) GetAgencyMembers(_ context.Context, req *userv1.GetAgencyMembersRequest) (*userv1.GetAgencyMembersResponse, error) {
f.lastMembers = req
if f.membersErr != nil {
return nil, f.membersErr
}
if f.membersResp != nil {
return f.membersResp, nil
}
return &userv1.GetAgencyMembersResponse{}, nil
}
func (f *fakeUserHostClient) GetAgencyApplications(_ context.Context, req *userv1.GetAgencyApplicationsRequest) (*userv1.GetAgencyApplicationsResponse, error) {
f.lastApplications = req
if f.applicationsErr != nil {
return nil, f.applicationsErr
}
if f.applicationsResp != nil {
return f.applicationsResp, nil
}
return &userv1.GetAgencyApplicationsResponse{}, nil
}
func (f *fakeUserHostClient) ReviewAgencyApplication(_ context.Context, req *userv1.ReviewAgencyApplicationRequest) (*userv1.ReviewAgencyApplicationResponse, error) {
f.lastReview = req
if f.reviewErr != nil {
return nil, f.reviewErr
}
if f.reviewResp != nil {
return f.reviewResp, nil
}
return &userv1.ReviewAgencyApplicationResponse{}, nil
}
func (f *fakeUserHostClient) KickAgencyHost(_ context.Context, req *userv1.KickAgencyHostRequest) (*userv1.KickAgencyHostResponse, error) {
f.lastKick = req
if f.kickErr != nil {
return nil, f.kickErr
}
if f.kickResp != nil {
return f.kickResp, nil
}
return &userv1.KickAgencyHostResponse{}, nil
}
func (f *fakeWalletClient) GetBalances(_ context.Context, req *walletv1.GetBalancesRequest) (*walletv1.GetBalancesResponse, error) {
f.last = req
f.balanceRequests = append(f.balanceRequests, req)
if f.err != nil {
return nil, f.err
}
if f.balancesByUserID != nil {
if resp, ok := f.balancesByUserID[req.GetUserId()]; ok {
return resp, nil
}
}
if f.resp != nil {
return f.resp, nil
}
@ -1382,6 +1468,21 @@ func (f *fakeWalletClient) EquipUserResource(context.Context, *walletv1.EquipUse
return &walletv1.EquipUserResourceResponse{}, nil
}
func (f *fakeWalletClient) UnequipUserResource(context.Context, *walletv1.UnequipUserResourceRequest) (*walletv1.UnequipUserResourceResponse, error) {
return &walletv1.UnequipUserResourceResponse{}, nil
}
func (f *fakeWalletClient) BatchGetUserEquippedResources(_ context.Context, req *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error) {
f.lastBatchEquipped = req
if f.err != nil {
return nil, f.err
}
if f.batchEquippedResp != nil {
return f.batchEquippedResp, nil
}
return &walletv1.BatchGetUserEquippedResourcesResponse{}, nil
}
func (f *fakeWalletClient) GetRedPacketConfig(context.Context, *walletv1.GetRedPacketConfigRequest) (*walletv1.GetRedPacketConfigResponse, error) {
return &walletv1.GetRedPacketConfigResponse{}, nil
}
@ -1822,6 +1923,23 @@ func TestSendGiftDoesNotInjectHostPeriodScopeForNonHost(t *testing.T) {
}
}
func TestSendGiftForwardsMultipleTargetUserIDs(t *testing.T) {
roomClient := &fakeRoomClient{}
router := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001}).Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-gift-multi","target_user_ids":[43,44],"gift_id":"rose","gift_count":2}`)))
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
if roomClient.lastGift == nil || roomClient.lastGift.GetTargetUserId() != 43 || len(roomClient.lastGift.GetTargetUserIds()) != 2 || roomClient.lastGift.GetTargetUserIds()[0] != 43 || roomClient.lastGift.GetTargetUserIds()[1] != 44 {
t.Fatalf("gateway must forward all target_user_ids: %+v", roomClient.lastGift)
}
}
func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T) {
previousNow := roomapi.TimeNow
roomapi.TimeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
@ -3062,6 +3180,182 @@ func TestApplyToHostAgencyPropagatesClientCommandID(t *testing.T) {
}
}
func TestAgencyCenterOverviewUsesOwnerAgencySalaryAndPendingApplications(t *testing.T) {
hostClient := &fakeUserHostClient{
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: true, AgencyId: 7001},
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{
AgencyId: 7001,
OwnerUserId: 42,
RegionId: 30,
ParentBdUserId: 77,
Name: "Yumi Star Agency",
Status: "active",
HostCount: 3,
}},
applicationsResp: &userv1.GetAgencyApplicationsResponse{Applications: []*userv1.AgencyApplication{
{ApplicationId: 9001, ApplicantUserId: 100, AgencyId: 7001, Status: "pending"},
{ApplicationId: 9002, ApplicantUserId: 101, AgencyId: 7001, Status: "pending"},
}},
}
walletClient := &fakeWalletClient{balancesByUserID: map[int64]*walletv1.GetBalancesResponse{
42: {Balances: []*walletv1.AssetBalance{{AssetType: "AGENCY_SALARY_USD", AvailableAmount: 1288075}}},
}}
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
77: {UserId: 77, DisplayUserId: "880001", Username: "Mila BD", Avatar: "https://cdn.example/bd.png"},
}}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
handler.SetUserHostClient(hostClient)
handler.SetWalletClient(walletClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/agency-center/overview", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
request.Header.Set("X-Request-ID", "req-agency-center-overview")
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
if hostClient.lastRoleSummary == nil || hostClient.lastRoleSummary.GetUserId() != 42 || hostClient.lastGetAgency == nil || hostClient.lastGetAgency.GetAgencyId() != 7001 {
t.Fatalf("agency owner resolution mismatch: role=%+v agency=%+v", hostClient.lastRoleSummary, hostClient.lastGetAgency)
}
if walletClient.last == nil || walletClient.last.GetUserId() != 42 || len(walletClient.last.GetAssetTypes()) != 1 || walletClient.last.GetAssetTypes()[0] != "AGENCY_SALARY_USD" {
t.Fatalf("agency salary request mismatch: %+v", walletClient.last)
}
if hostClient.lastApplications == nil || hostClient.lastApplications.GetAgencyId() != 7001 || hostClient.lastApplications.GetStatus() != "pending" {
t.Fatalf("pending applications request mismatch: %+v", hostClient.lastApplications)
}
var response httpkit.ResponseEnvelope
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response failed: %v", err)
}
data := response.Data.(map[string]any)
salary := data["salary"].(map[string]any)
bd := data["bd"].(map[string]any)
if salary["asset_type"] != "AGENCY_SALARY_USD" || salary["available_amount"] != float64(1288075) || salary["display_amount"] != 12880.75 || data["pending_application_count"] != float64(2) || bd["display_user_id"] != "880001" {
t.Fatalf("overview response mismatch: %+v", data)
}
}
func TestAgencyCenterOverviewRejectsNonOwner(t *testing.T) {
hostClient := &fakeUserHostClient{roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: false, AgencyId: 7001}}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
handler.SetUserHostClient(hostClient)
handler.SetWalletClient(&fakeWalletClient{})
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/agency-center/overview", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusForbidden {
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
var response httpkit.ResponseEnvelope
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response failed: %v", err)
}
if response.Code != httpkit.CodePermissionDenied || hostClient.lastGetAgency != nil {
t.Fatalf("non-owner response mismatch: response=%+v getAgency=%+v", response, hostClient.lastGetAgency)
}
}
func TestAgencyCenterHostsReturnsActiveHostSalary(t *testing.T) {
hostClient := &fakeUserHostClient{
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: true, AgencyId: 7001},
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{AgencyId: 7001, OwnerUserId: 42, Status: "active"}},
membersResp: &userv1.GetAgencyMembersResponse{Memberships: []*userv1.AgencyMembership{
{MembershipId: 1, AgencyId: 7001, HostUserId: 42, MembershipType: "owner", Status: "active", JoinedAtMs: 10},
{MembershipId: 2, AgencyId: 7001, HostUserId: 100, MembershipType: "host", Status: "active", JoinedAtMs: 20},
{MembershipId: 3, AgencyId: 7001, HostUserId: 101, MembershipType: "host", Status: "active", JoinedAtMs: 30},
}},
}
walletClient := &fakeWalletClient{balancesByUserID: map[int64]*walletv1.GetBalancesResponse{
100: {Balances: []*walletv1.AssetBalance{{AssetType: "HOST_SALARY_USD", AvailableAmount: 305025}}},
101: {Balances: []*walletv1.AssetBalance{{AssetType: "HOST_SALARY_USD", AvailableAmount: 428050}}},
}}
profileClient := &fakeUserProfileClient{usersByID: map[int64]*userv1.User{
100: {UserId: 100, DisplayUserId: "163100", Username: "Lina_voice", Avatar: "https://cdn.example/lina.png"},
101: {UserId: 101, DisplayUserId: "163101", Username: "Nora_live", Avatar: "https://cdn.example/nora.png"},
}}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
handler.SetUserHostClient(hostClient)
handler.SetWalletClient(walletClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/agency-center/hosts", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
if hostClient.lastMembers == nil || hostClient.lastMembers.GetAgencyId() != 7001 || hostClient.lastMembers.GetStatus() != "active" {
t.Fatalf("members request mismatch: %+v", hostClient.lastMembers)
}
if len(walletClient.balanceRequests) != 2 || walletClient.balanceRequests[0].GetUserId() != 100 || walletClient.balanceRequests[1].GetUserId() != 101 {
t.Fatalf("host salary requests mismatch: %+v", walletClient.balanceRequests)
}
var response httpkit.ResponseEnvelope
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
t.Fatalf("decode response failed: %v", err)
}
data := response.Data.(map[string]any)
items := data["items"].([]any)
first := items[0].(map[string]any)
salary := first["salary"].(map[string]any)
if data["total"] != float64(2) || first["host_user_id"] != "100" || first["display_user_id"] != "163100" || first["username"] != "Lina_voice" || salary["display_amount"] != 3050.25 {
t.Fatalf("hosts response mismatch: %+v", data)
}
}
func TestAgencyCenterReviewAndRemoveForwardOwnerCommands(t *testing.T) {
hostClient := &fakeUserHostClient{
roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: true, AgencyId: 7001},
getAgencyResp: &userv1.GetAgencyResponse{Agency: &userv1.Agency{AgencyId: 7001, OwnerUserId: 42, Status: "active"}},
reviewResp: &userv1.ReviewAgencyApplicationResponse{Application: &userv1.AgencyApplication{
ApplicationId: 9001,
AgencyId: 7001,
Status: "approved",
}},
kickResp: &userv1.KickAgencyHostResponse{Membership: &userv1.AgencyMembership{
MembershipId: 2,
AgencyId: 7001,
HostUserId: 100,
Status: "ended",
}},
}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
handler.SetUserHostClient(hostClient)
handler.SetWalletClient(&fakeWalletClient{})
router := handler.Routes(auth.NewVerifier("secret"))
reviewRequest := httptest.NewRequest(http.MethodPost, "/api/v1/agency-center/applications/9001/review", bytes.NewReader([]byte(`{"command_id":"cmd-review","decision":"approve","reason":"ok"}`)))
reviewRequest.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
reviewRecorder := httptest.NewRecorder()
router.ServeHTTP(reviewRecorder, reviewRequest)
if reviewRecorder.Code != http.StatusOK {
t.Fatalf("review status mismatch: got %d body=%s", reviewRecorder.Code, reviewRecorder.Body.String())
}
if hostClient.lastReview == nil || hostClient.lastReview.GetReviewerUserId() != 42 || hostClient.lastReview.GetApplicationId() != 9001 || hostClient.lastReview.GetCommandId() != "cmd-review" || hostClient.lastReview.GetDecision() != "approve" {
t.Fatalf("review request mismatch: %+v", hostClient.lastReview)
}
removeRequest := httptest.NewRequest(http.MethodPost, "/api/v1/agency-center/hosts/100/remove", bytes.NewReader([]byte(`{"command_id":"cmd-remove","reason":"agency_center"}`)))
removeRequest.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
removeRecorder := httptest.NewRecorder()
router.ServeHTTP(removeRecorder, removeRequest)
if removeRecorder.Code != http.StatusOK {
t.Fatalf("remove status mismatch: got %d body=%s", removeRecorder.Code, removeRecorder.Body.String())
}
if hostClient.lastKick == nil || hostClient.lastKick.GetOperatorUserId() != 42 || hostClient.lastKick.GetAgencyId() != 7001 || hostClient.lastKick.GetHostUserId() != 100 || hostClient.lastKick.GetCommandId() != "cmd-remove" {
t.Fatalf("remove request mismatch: %+v", hostClient.lastKick)
}
}
func TestConfirmMicPublishingForwardsSessionVersionAndEventTime(t *testing.T) {
client := &fakeRoomClient{}
router := NewHandler(client).Routes(auth.NewVerifier("secret"))
@ -3722,6 +4016,115 @@ func TestAchievementAndBadgeRoutesEmbedResourceMaterials(t *testing.T) {
}
}
func TestAppearanceAggregatesEquippedResourcesAndBadges(t *testing.T) {
frameResource := &walletv1.Resource{
ResourceId: 8101,
ResourceCode: "frame_gold",
ResourceType: "avatar_frame",
Name: "Gold Frame",
AssetUrl: "https://cdn.example/frame.png",
AnimationUrl: "https://cdn.example/frame.svga",
MetadataJson: `{"level":1}`,
GrantStrategy: "set_active_flag",
Status: "active",
}
vehicleResource := &walletv1.Resource{
ResourceId: 8102,
ResourceCode: "vehicle_gold",
ResourceType: "vehicle",
Name: "Gold Vehicle",
AssetUrl: "https://cdn.example/vehicle.png",
AnimationUrl: "https://cdn.example/vehicle.svga",
GrantStrategy: "set_active_flag",
Status: "active",
}
badgeResource := &walletv1.Resource{
ResourceId: 8103,
ResourceCode: "badge_host",
ResourceType: "badge",
Name: "Host Badge",
AssetUrl: "https://cdn.example/badge.png",
GrantStrategy: "set_active_flag",
Status: "active",
}
walletClient := &fakeWalletClient{
batchEquippedResp: &walletv1.BatchGetUserEquippedResourcesResponse{
Users: []*walletv1.UserEquippedResources{{
UserId: 42,
Resources: []*walletv1.UserResourceEntitlement{
{UserId: 42, EntitlementId: "ent-frame", ResourceId: 8101, Resource: frameResource, Equipped: true, ExpiresAtMs: 1999999999999},
{UserId: 42, EntitlementId: "ent-vehicle", ResourceId: 8102, Resource: vehicleResource, Equipped: true, ExpiresAtMs: 1999999999999},
{UserId: 42, EntitlementId: "ent-badge", ResourceId: 8103, Resource: badgeResource, Equipped: true, ExpiresAtMs: 1999999999999},
},
}},
},
resourcesByID: map[int64]*walletv1.Resource{8103: badgeResource},
}
achievementClient := &fakeAchievementClient{
badgesResp: &activityv1.ListMyBadgesResponse{
StripBadges: []*activityv1.BadgeDisplayItem{{
Slot: "strip",
Position: 1,
BadgeForm: "icon",
ResourceId: 8103,
EntitlementId: "ent-badge",
SourceType: "achievement",
SourceId: "ach-host",
PinMode: "auto",
UpdatedAtMs: 1778256000000,
}},
ServerTimeMs: 1778256000000,
},
}
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
handler.SetWalletClient(walletClient)
handler.SetAchievementClient(achievementClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodGet, "/api/v1/appearance", nil)
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
recorder := httptest.NewRecorder()
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("appearance status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
}
var envelope struct {
Code string `json:"code"`
Data struct {
UserID string `json:"user_id"`
AvatarFrame struct {
ResourceID int64 `json:"resource_id"`
AnimationURL string `json:"animation_url"`
} `json:"avatar_frame"`
Vehicle struct {
ResourceID int64 `json:"resource_id"`
AnimationURL string `json:"animation_url"`
} `json:"vehicle"`
EquippedBadges []struct {
ResourceID int64 `json:"resource_id"`
} `json:"equipped_badges"`
Badges []struct {
ResourceID int64 `json:"resource_id"`
Resource struct {
AssetURL string `json:"asset_url"`
} `json:"resource"`
} `json:"badges"`
} `json:"data"`
}
if err := json.NewDecoder(recorder.Body).Decode(&envelope); err != nil {
t.Fatalf("decode appearance response failed: %v", err)
}
if walletClient.lastBatchEquipped == nil || walletClient.lastBatchEquipped.GetUserIds()[0] != 42 {
t.Fatalf("appearance should batch query equipped resources: %+v", walletClient.lastBatchEquipped)
}
if achievementClient.lastBadges == nil || achievementClient.lastBadges.GetUserId() != 42 {
t.Fatalf("appearance should query displayed badges: %+v", achievementClient.lastBadges)
}
if envelope.Code != httpkit.CodeOK || envelope.Data.UserID != "42" || envelope.Data.AvatarFrame.AnimationURL != "https://cdn.example/frame.svga" || envelope.Data.Vehicle.ResourceID != 8102 || len(envelope.Data.EquippedBadges) != 1 || len(envelope.Data.Badges) != 1 || envelope.Data.Badges[0].Resource.AssetURL != "https://cdn.example/badge.png" {
t.Fatalf("appearance response mismatch: %+v", envelope)
}
}
func TestListEmojiPacksFiltersRegionAndReturnsAssets(t *testing.T) {
walletClient := &fakeWalletClient{
listResourcesResp: &walletv1.ListResourcesResponse{

View File

@ -18,6 +18,7 @@ type Handler struct {
userSocialClient client.UserSocialClient
userHostClient client.UserHostClient
walletClient client.WalletClient
achievementClient client.AchievementClient
rtcTokenConfig tencentrtc.TokenConfig
}
@ -29,6 +30,7 @@ type Config struct {
UserSocialClient client.UserSocialClient
UserHostClient client.UserHostClient
WalletClient client.WalletClient
AchievementClient client.AchievementClient
RTCTokenConfig tencentrtc.TokenConfig
}
@ -41,6 +43,7 @@ func New(config Config) *Handler {
userSocialClient: config.UserSocialClient,
userHostClient: config.UserHostClient,
walletClient: config.WalletClient,
achievementClient: config.AchievementClient,
rtcTokenConfig: config.RTCTokenConfig,
}
}

View File

@ -14,6 +14,7 @@ import (
"hyapp/pkg/xerr"
"hyapp/services/gateway-service/internal/auth"
activityv1 "hyapp.local/api/proto/activity/v1"
roomv1 "hyapp.local/api/proto/room/v1"
userv1 "hyapp.local/api/proto/user/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
@ -839,6 +840,8 @@ func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
Role: body.Role,
Password: body.Password,
// 座驾快照在进房命令提交前生成room-service 只保存快照,不回查 wallet。
EntryVehicle: h.joinRoomEntryVehicleSnapshot(request),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
@ -847,6 +850,55 @@ func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
httpkit.WriteOK(writer, request, h.joinRoomInitialData(request, body.RoomID, resp))
}
func (h *Handler) joinRoomEntryVehicleSnapshot(request *http.Request) *roomv1.RoomEntryVehicleSnapshot {
if h.walletClient == nil {
// 座驾只影响进房展示,不应该因为 wallet 暂不可用阻断 JoinRoom。
return nil
}
userID := auth.UserIDFromContext(request.Context())
if userID <= 0 {
return nil
}
resp, err := h.walletClient.BatchGetUserEquippedResources(request.Context(), &walletv1.BatchGetUserEquippedResourcesRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserIds: []int64{userID},
ResourceTypes: []string{"vehicle"},
})
if err != nil {
// 佩戴态增强失败时降级为无座驾进房;房间 presence 仍由 room-service 提交。
return nil
}
for _, user := range resp.GetUsers() {
if user.GetUserId() != userID {
// batch 响应按用户分组;忽略非当前用户,避免串用户座驾。
continue
}
for _, item := range user.GetResources() {
if item.GetResource().GetResourceType() != "vehicle" {
continue
}
resource := item.GetResource()
if resource.GetResourceId() <= 0 {
continue
}
// 这里复制的是资源素材和 entitlement 有效期,不把 wallet 的内部权益结构透传给 room-service。
return &roomv1.RoomEntryVehicleSnapshot{
ResourceId: resource.GetResourceId(),
ResourceCode: resource.GetResourceCode(),
Name: resource.GetName(),
AssetUrl: resource.GetAssetUrl(),
PreviewUrl: resource.GetPreviewUrl(),
AnimationUrl: resource.GetAnimationUrl(),
MetadataJson: resource.GetMetadataJson(),
EntitlementId: item.GetEntitlementId(),
ExpiresAtMs: item.GetExpiresAtMs(),
}
}
}
return nil
}
func (h *Handler) joinRoomInitialData(request *http.Request, requestedRoomID string, resp *roomv1.JoinRoomResponse) joinRoomData {
if resp == nil {
return joinRoomData{}
@ -1255,7 +1307,7 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
targetType = "user"
}
targetUserIDs := normalizeGiftTargetUserIDs(body.TargetUserID, body.TargetUserIDs)
if targetType == "user" && len(targetUserIDs) != 1 {
if targetType == "user" && len(targetUserIDs) == 0 {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
@ -1392,12 +1444,16 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
// 房间资料是展示增强,失败时保留主房间数据,让客户端后续重试 batch 接口。
return nil
}
// 房间资料高频读取,头像框/座驾用 wallet 批量查badge 当前 activity 只有单用户查询,单独降级处理。
appearanceMap := h.roomAppearanceResourceMap(request, userIDs)
badgeMap := h.roomBadgeDisplayMap(request, userIDs)
profiles := make(map[int64]roomDisplayProfileData, len(resp.GetUsers()))
for _, userID := range userIDs {
user := resp.GetUsers()[userID]
if user == nil {
continue
}
appearance := appearanceMap[userID]
profiles[userID] = roomDisplayProfileData{
UserID: formatOptionalUserID(user.GetUserId()),
Username: user.GetUsername(),
@ -1411,15 +1467,177 @@ func (h *Handler) roomDisplayProfileMap(request *http.Request, userIDs []int64)
CountryFlag: roomProfileCountryFlag(user.GetCountry()),
VIP: map[string]any{},
Level: map[string]any{},
Badges: []map[string]any{},
AvatarFrame: map[string]any{},
Vehicle: map[string]any{},
Badges: badgeMap[userID],
AvatarFrame: appearance.AvatarFrame,
Vehicle: appearance.Vehicle,
Charm: 0,
}
// 显式补空对象/空数组,避免客户端在房间资料中同时处理 null 和空值。
if profiles[userID].Badges == nil {
profile := profiles[userID]
profile.Badges = []map[string]any{}
profiles[userID] = profile
}
if profiles[userID].AvatarFrame == nil {
profile := profiles[userID]
profile.AvatarFrame = map[string]any{}
profiles[userID] = profile
}
if profiles[userID].Vehicle == nil {
profile := profiles[userID]
profile.Vehicle = map[string]any{}
profiles[userID] = profile
}
}
return profiles
}
type roomAppearanceResources struct {
AvatarFrame map[string]any
Vehicle map[string]any
}
func (h *Handler) roomAppearanceResourceMap(request *http.Request, userIDs []int64) map[int64]roomAppearanceResources {
result := make(map[int64]roomAppearanceResources, len(userIDs))
if h.walletClient == nil || len(userIDs) == 0 {
return result
}
// wallet 在该 RPC 内做过期佩戴过滤,所以 roomDisplayProfileMap 不需要重复判断 expires_at_ms。
resp, err := h.walletClient.BatchGetUserEquippedResources(request.Context(), &walletv1.BatchGetUserEquippedResourcesRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserIds: userIDs,
ResourceTypes: []string{"avatar_frame", "vehicle"},
})
if err != nil {
// 装扮是展示增强,失败不影响房间主数据返回。
return result
}
for _, user := range resp.GetUsers() {
if user.GetUserId() <= 0 {
continue
}
current := result[user.GetUserId()]
for _, item := range user.GetResources() {
// 同一资源类型只保留当前佩戴的一个权益wallet equipment 主键保证唯一性。
resource := roomAppearanceResourceFromEntitlement(item)
switch item.GetResource().GetResourceType() {
case "avatar_frame":
current.AvatarFrame = resource
case "vehicle":
current.Vehicle = resource
}
}
result[user.GetUserId()] = current
}
return result
}
func (h *Handler) roomBadgeDisplayMap(request *http.Request, userIDs []int64) map[int64][]map[string]any {
result := make(map[int64][]map[string]any, len(userIDs))
if h.achievementClient == nil || len(userIDs) == 0 {
return result
}
// activity 目前没有 badge 批量接口;这里逐用户读取,但所有 badge 资源素材统一去重后解析。
raw := make(map[int64][]*activityv1.BadgeDisplayItem, len(userIDs))
resourceIDs := make([]int64, 0)
for _, userID := range userIDs {
resp, err := h.achievementClient.ListMyBadges(request.Context(), &activityv1.ListMyBadgesRequest{
Meta: httpkit.ActivityMeta(request),
UserId: userID,
})
if err != nil {
// 单个用户 badge 投影失败时只影响该用户徽章,不拖垮整个房间资料响应。
continue
}
items := resp.GetStripBadges()
raw[userID] = items
for _, item := range items {
resourceIDs = append(resourceIDs, item.GetResourceId())
}
}
resources := h.roomResourcesByID(request, resourceIDs)
for userID, items := range raw {
result[userID] = roomBadgeItemsFromProto(items, resources)
}
return result
}
func (h *Handler) roomResourcesByID(request *http.Request, ids []int64) map[int64]map[string]any {
ids = uniquePositiveUserIDs(ids)
result := make(map[int64]map[string]any, len(ids))
if h.walletClient == nil || len(ids) == 0 {
return result
}
// badge display item 只带 resource_id这里补齐客户端渲染需要的 asset/animation/metadata。
for _, id := range ids {
resp, err := h.walletClient.GetResource(request.Context(), &walletv1.GetResourceRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
ResourceId: id,
})
if err != nil {
// 素材缺失时保留 badge item 本体,客户端至少能根据 resource_id 做兜底展示。
continue
}
resource := roomAppearanceResourceFromProto(resp.GetResource(), "", 0)
if resourceID, ok := resource["resource_id"].(int64); ok && resourceID > 0 {
result[resourceID] = resource
}
}
return result
}
func roomBadgeItemsFromProto(items []*activityv1.BadgeDisplayItem, resources map[int64]map[string]any) []map[string]any {
result := make([]map[string]any, 0, len(items))
for _, item := range items {
if item == nil {
continue
}
data := map[string]any{
"slot": item.GetSlot(),
"position": item.GetPosition(),
"badge_form": item.GetBadgeForm(),
"resource_id": item.GetResourceId(),
"entitlement_id": item.GetEntitlementId(),
"source_type": item.GetSourceType(),
"source_id": item.GetSourceId(),
"pin_mode": item.GetPinMode(),
"updated_at_ms": item.GetUpdatedAtMs(),
}
if resource, ok := resources[item.GetResourceId()]; ok {
data["resource"] = resource
}
result = append(result, data)
}
return result
}
func roomAppearanceResourceFromEntitlement(item *walletv1.UserResourceEntitlement) map[string]any {
if item == nil {
return map[string]any{}
}
return roomAppearanceResourceFromProto(item.GetResource(), item.GetEntitlementId(), item.GetExpiresAtMs())
}
func roomAppearanceResourceFromProto(item *walletv1.Resource, entitlementID string, expiresAtMS int64) map[string]any {
if item == nil || item.GetResourceId() <= 0 {
return map[string]any{}
}
return map[string]any{
"resource_id": item.GetResourceId(),
"resource_code": item.GetResourceCode(),
"resource_type": item.GetResourceType(),
"name": item.GetName(),
"asset_url": item.GetAssetUrl(),
"preview_url": item.GetPreviewUrl(),
"animation_url": item.GetAnimationUrl(),
"metadata_json": item.GetMetadataJson(),
"entitlement_id": entitlementID,
"expires_at_ms": expiresAtMS,
}
}
func roomProfileAgeFromBirth(birth string, now time.Time) int32 {
birth = strings.TrimSpace(birth)
if birth == "" {

View File

@ -29,6 +29,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
UserSocialClient: h.userSocialClient,
UserHostClient: h.userHostClient,
WalletClient: h.walletClient,
AchievementClient: h.achievementClient,
RTCTokenConfig: tencentrtc.TokenConfig{
Enabled: h.tencentRTC.Enabled,
SDKAppID: h.tencentRTC.SDKAppID,
@ -54,6 +55,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
UserCountryClient: h.userCountryClient,
UserHostClient: h.userHostClient,
WalletClient: h.walletClient,
AchievementClient: h.achievementClient,
BroadcastClient: h.broadcastClient,
})
messageAPI := messageapi.New(messageapi.Config{
@ -112,6 +114,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
userHandlers.GetMyGiftWall = walletAPI.GetMyGiftWall
userHandlers.ListMyResources = resourceAPI.ListMyResources
userHandlers.EquipMyResource = resourceAPI.EquipMyResource
userHandlers.UnequipMyResource = resourceAPI.UnequipMyResource
return httproutes.New(httproutes.Config{
PublicWrap: h.publicAPIHandler,

View File

@ -0,0 +1,422 @@
package userapi
import (
"net/http"
"strings"
"time"
userv1 "hyapp.local/api/proto/user/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
)
const (
agencySalaryAssetType = "AGENCY_SALARY_USD"
hostSalaryAssetType = "HOST_SALARY_USD"
agencyStatusActive = "active"
membershipStatusActive = "active"
membershipTypeOwner = "owner"
applicationStatusPending = "pending"
)
type agencyCenterSalaryData struct {
AssetType string `json:"asset_type"`
AvailableAmount int64 `json:"available_amount"`
FrozenAmount int64 `json:"frozen_amount"`
DisplayAmount float64 `json:"display_amount"`
}
type agencyCenterUserData struct {
UserID string `json:"user_id"`
DisplayUserID string `json:"display_user_id"`
Username string `json:"username"`
Avatar string `json:"avatar,omitempty"`
}
type agencyCenterHostData struct {
MembershipID string `json:"membership_id"`
AgencyID string `json:"agency_id"`
HostUserID string `json:"host_user_id"`
DisplayUserID string `json:"display_user_id"`
Username string `json:"username"`
Avatar string `json:"avatar,omitempty"`
RegionID int64 `json:"region_id"`
MembershipType string `json:"membership_type"`
Status string `json:"status"`
JoinedAtMS int64 `json:"joined_at_ms"`
Salary agencyCenterSalaryData `json:"salary"`
}
type agencyCenterApplicationData struct {
ApplicationID string `json:"application_id"`
CommandID string `json:"command_id"`
ApplicantUserID string `json:"applicant_user_id"`
AgencyID string `json:"agency_id"`
RegionID int64 `json:"region_id"`
Status string `json:"status"`
ReviewReason string `json:"review_reason,omitempty"`
ReviewedAtMS int64 `json:"reviewed_at_ms,omitempty"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
Applicant agencyCenterUserData `json:"applicant"`
}
func (h *Handler) getAgencyCenterOverview(writer http.ResponseWriter, request *http.Request) {
agency, ok := h.resolveCurrentOwnerAgency(writer, request)
if !ok {
return
}
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
salary, err := h.salaryBalance(request, agency.GetOwnerUserId(), agencySalaryAssetType)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
applications, err := h.userHostClient.GetAgencyApplications(request.Context(), &userv1.GetAgencyApplicationsRequest{
Meta: httpkit.UserMeta(request, ""),
AgencyId: agency.GetAgencyId(),
Status: applicationStatusPending,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
var bd *agencyCenterUserData
if agency.GetParentBdUserId() > 0 {
profiles, err := h.userProfiles(request, []int64{agency.GetParentBdUserId()})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
if profile, exists := profiles[agency.GetParentBdUserId()]; exists {
bd = &profile
}
}
httpkit.WriteOK(writer, request, map[string]any{
"agency": hostAgencyFromProto(agency),
"bd": bd,
"salary": salary,
"pending_application_count": len(applications.GetApplications()),
"server_time_ms": time.Now().UnixMilli(),
})
}
func (h *Handler) listAgencyCenterHosts(writer http.ResponseWriter, request *http.Request) {
agency, ok := h.resolveCurrentOwnerAgency(writer, request)
if !ok {
return
}
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
resp, err := h.userHostClient.GetAgencyMembers(request.Context(), &userv1.GetAgencyMembersRequest{
Meta: httpkit.UserMeta(request, ""),
AgencyId: agency.GetAgencyId(),
Status: membershipStatusActive,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
memberships := make([]*userv1.AgencyMembership, 0, len(resp.GetMemberships()))
userIDs := make([]int64, 0, len(resp.GetMemberships()))
for _, membership := range resp.GetMemberships() {
if membership.GetHostUserId() == agency.GetOwnerUserId() || membership.GetMembershipType() == membershipTypeOwner {
continue
}
memberships = append(memberships, membership)
userIDs = append(userIDs, membership.GetHostUserId())
}
profiles, err := h.userProfiles(request, userIDs)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
items := make([]agencyCenterHostData, 0, len(memberships))
for _, membership := range memberships {
salary, err := h.salaryBalance(request, membership.GetHostUserId(), hostSalaryAssetType)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
profile := profiles[membership.GetHostUserId()]
items = append(items, agencyCenterHostData{
MembershipID: int64String(membership.GetMembershipId()),
AgencyID: int64String(membership.GetAgencyId()),
HostUserID: int64String(membership.GetHostUserId()),
DisplayUserID: profile.DisplayUserID,
Username: profile.Username,
Avatar: profile.Avatar,
RegionID: membership.GetRegionId(),
MembershipType: membership.GetMembershipType(),
Status: membership.GetStatus(),
JoinedAtMS: membership.GetJoinedAtMs(),
Salary: salary,
})
}
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
}
func (h *Handler) listAgencyCenterApplications(writer http.ResponseWriter, request *http.Request) {
agency, ok := h.resolveCurrentOwnerAgency(writer, request)
if !ok {
return
}
status := strings.TrimSpace(request.URL.Query().Get("status"))
if status == "" {
status = applicationStatusPending
}
if status != applicationStatusPending && status != "approved" && status != "rejected" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.userHostClient.GetAgencyApplications(request.Context(), &userv1.GetAgencyApplicationsRequest{
Meta: httpkit.UserMeta(request, ""),
AgencyId: agency.GetAgencyId(),
Status: status,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
userIDs := make([]int64, 0, len(resp.GetApplications()))
for _, application := range resp.GetApplications() {
userIDs = append(userIDs, application.GetApplicantUserId())
}
profiles, err := h.userProfiles(request, userIDs)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
items := make([]agencyCenterApplicationData, 0, len(resp.GetApplications()))
for _, application := range resp.GetApplications() {
items = append(items, agencyCenterApplicationData{
ApplicationID: int64String(application.GetApplicationId()),
CommandID: application.GetCommandId(),
ApplicantUserID: int64String(application.GetApplicantUserId()),
AgencyID: int64String(application.GetAgencyId()),
RegionID: application.GetRegionId(),
Status: application.GetStatus(),
ReviewReason: application.GetReviewReason(),
ReviewedAtMS: application.GetReviewedAtMs(),
CreatedAtMS: application.GetCreatedAtMs(),
UpdatedAtMS: application.GetUpdatedAtMs(),
Applicant: profiles[application.GetApplicantUserId()],
})
}
httpkit.WriteOK(writer, request, map[string]any{"items": items, "total": len(items)})
}
func (h *Handler) reviewAgencyCenterApplication(writer http.ResponseWriter, request *http.Request) {
applicationID, ok := httpkit.PositiveInt64PathValue(request, "application_id")
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
if _, ok := h.resolveCurrentOwnerAgency(writer, request); !ok {
return
}
var body struct {
CommandID string `json:"command_id"`
Decision string `json:"decision"`
Reason string `json:"reason"`
}
if !httpkit.Decode(writer, request, &body) {
return
}
commandID := strings.TrimSpace(body.CommandID)
decision := strings.ToLower(strings.TrimSpace(body.Decision))
if commandID == "" || (decision != "approve" && decision != "reject") {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.userHostClient.ReviewAgencyApplication(request.Context(), &userv1.ReviewAgencyApplicationRequest{
Meta: httpkit.UserMeta(request, ""),
CommandId: commandID,
ReviewerUserId: auth.UserIDFromContext(request.Context()),
ApplicationId: applicationID,
Decision: decision,
Reason: strings.TrimSpace(body.Reason),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{
"application": hostAgencyApplicationFromProto(resp.GetApplication()),
"membership": agencyMembershipData(resp.GetMembership()),
})
}
func (h *Handler) removeAgencyCenterHost(writer http.ResponseWriter, request *http.Request) {
hostUserID, ok := httpkit.PositiveInt64PathValue(request, "host_user_id")
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
agency, ok := h.resolveCurrentOwnerAgency(writer, request)
if !ok {
return
}
var body struct {
CommandID string `json:"command_id"`
Reason string `json:"reason"`
}
if !httpkit.Decode(writer, request, &body) {
return
}
commandID := strings.TrimSpace(body.CommandID)
if commandID == "" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.userHostClient.KickAgencyHost(request.Context(), &userv1.KickAgencyHostRequest{
Meta: httpkit.UserMeta(request, ""),
CommandId: commandID,
OperatorUserId: auth.UserIDFromContext(request.Context()),
AgencyId: agency.GetAgencyId(),
HostUserId: hostUserID,
Reason: strings.TrimSpace(body.Reason),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{"membership": agencyMembershipData(resp.GetMembership())})
}
func (h *Handler) resolveCurrentOwnerAgency(writer http.ResponseWriter, request *http.Request) (*userv1.Agency, bool) {
if h.userHostClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return nil, false
}
userID := auth.UserIDFromContext(request.Context())
roleResp, err := h.userHostClient.GetUserRoleSummary(request.Context(), &userv1.GetUserRoleSummaryRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: userID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return nil, false
}
summary := roleResp.GetSummary()
if summary == nil || !summary.GetIsManager() || summary.GetAgencyId() <= 0 {
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
return nil, false
}
agencyResp, err := h.userHostClient.GetAgency(request.Context(), &userv1.GetAgencyRequest{
Meta: httpkit.UserMeta(request, ""),
AgencyId: summary.GetAgencyId(),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return nil, false
}
agency := agencyResp.GetAgency()
if agency == nil || agency.GetOwnerUserId() != userID || agency.GetStatus() != agencyStatusActive {
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
return nil, false
}
return agency, true
}
func (h *Handler) userProfiles(request *http.Request, userIDs []int64) (map[int64]agencyCenterUserData, error) {
profiles := make(map[int64]agencyCenterUserData, len(userIDs))
if len(userIDs) == 0 {
return profiles, nil
}
if h.userProfileClient == nil {
return profiles, nil
}
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
Meta: httpkit.UserMeta(request, ""),
UserIds: userIDs,
})
if err != nil {
return nil, err
}
for userID, user := range resp.GetUsers() {
profiles[userID] = agencyCenterUserFromProto(user)
}
for _, userID := range userIDs {
if _, exists := profiles[userID]; !exists {
profiles[userID] = agencyCenterUserData{UserID: int64String(userID)}
}
}
return profiles, nil
}
func (h *Handler) salaryBalance(request *http.Request, userID int64, assetType string) (agencyCenterSalaryData, error) {
resp, err := h.walletClient.GetBalances(request.Context(), &walletv1.GetBalancesRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
UserId: userID,
AssetTypes: []string{assetType},
AppCode: appcode.FromContext(request.Context()),
})
if err != nil {
return agencyCenterSalaryData{}, err
}
for _, balance := range resp.GetBalances() {
if balance.GetAssetType() == assetType {
return salaryDataFromProto(balance), nil
}
}
return agencyCenterSalaryData{AssetType: assetType}, nil
}
func salaryDataFromProto(balance *walletv1.AssetBalance) agencyCenterSalaryData {
if balance == nil {
return agencyCenterSalaryData{}
}
return agencyCenterSalaryData{
AssetType: balance.GetAssetType(),
AvailableAmount: balance.GetAvailableAmount(),
FrozenAmount: balance.GetFrozenAmount(),
DisplayAmount: float64(balance.GetAvailableAmount()) / 100,
}
}
func agencyCenterUserFromProto(user *userv1.User) agencyCenterUserData {
if user == nil {
return agencyCenterUserData{}
}
return agencyCenterUserData{
UserID: int64String(user.GetUserId()),
DisplayUserID: user.GetDisplayUserId(),
Username: user.GetUsername(),
Avatar: user.GetAvatar(),
}
}
func agencyMembershipData(membership *userv1.AgencyMembership) map[string]any {
if membership == nil {
return map[string]any{}
}
return map[string]any{
"membership_id": int64String(membership.GetMembershipId()),
"agency_id": int64String(membership.GetAgencyId()),
"host_user_id": int64String(membership.GetHostUserId()),
"region_id": membership.GetRegionId(),
"membership_type": membership.GetMembershipType(),
"status": membership.GetStatus(),
"joined_at_ms": membership.GetJoinedAtMs(),
"ended_at_ms": membership.GetEndedAtMs(),
}
}

View File

@ -0,0 +1,272 @@
package userapi
import (
"errors"
"net/http"
"strconv"
activityv1 "hyapp.local/api/proto/activity/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
)
const (
// /appearance 只暴露客户端能直接佩戴或展示的装扮维度;类型值保持和 wallet resource_type 一致。
appearanceResourceAvatarFrame = "avatar_frame"
appearanceResourceProfileCard = "profile_card"
appearanceResourceVehicle = "vehicle"
appearanceResourceBadge = "badge"
)
var errAppearanceUpstream = errors.New("upstream service error")
type appearanceData struct {
UserID string `json:"user_id"`
AvatarFrame map[string]any `json:"avatar_frame"`
ProfileCard map[string]any `json:"profile_card"`
Vehicle map[string]any `json:"vehicle"`
EquippedBadges []map[string]any `json:"equipped_badges"`
Badges []map[string]any `json:"badges"`
BadgeProfile appearanceBadgeData `json:"badge_profile"`
Resources []map[string]any `json:"resources"`
ServerTimeMS int64 `json:"server_time_ms"`
}
type appearanceBadgeData struct {
StripBadges []map[string]any `json:"strip_badges"`
ProfileTileBadges []map[string]any `json:"profile_tile_badges"`
HonorBadges []map[string]any `json:"honor_badges"`
ServerTimeMS int64 `json:"server_time_ms"`
}
func (h *Handler) getMyAppearance(writer http.ResponseWriter, request *http.Request) {
h.writeAppearance(writer, request, auth.UserIDFromContext(request.Context()))
}
func (h *Handler) getUserAppearance(writer http.ResponseWriter, request *http.Request) {
userID, ok := httpkit.PositiveInt64PathValue(request, "user_id")
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
h.writeAppearance(writer, request, userID)
}
func (h *Handler) writeAppearance(writer http.ResponseWriter, request *http.Request, userID int64) {
if userID <= 0 {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
// wallet 是装扮权益和当前佩戴的事实源;缺 wallet 时 appearance 不能返回可信数据。
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
data, err := h.appearanceForUser(request, userID)
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, data)
}
func (h *Handler) appearanceForUser(request *http.Request, userID int64) (appearanceData, error) {
// 先初始化为空对象/数组,保证客户端不需要处理 null 和字段缺失两套形态。
data := appearanceData{
UserID: strconv.FormatInt(userID, 10),
AvatarFrame: map[string]any{},
ProfileCard: map[string]any{},
Vehicle: map[string]any{},
EquippedBadges: []map[string]any{},
Badges: []map[string]any{},
Resources: []map[string]any{},
}
// 当前佩戴只走 BatchGetUserEquippedResources
// 1. wallet 内部会过滤过期佩戴;
// 2. 后续扩展批量 appearance 时可以复用同一 RPC不引入 N+1。
equipped, err := h.walletClient.BatchGetUserEquippedResources(request.Context(), &walletv1.BatchGetUserEquippedResourcesRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
UserIds: []int64{userID},
ResourceTypes: []string{appearanceResourceAvatarFrame, appearanceResourceProfileCard, appearanceResourceVehicle, appearanceResourceBadge},
})
if err != nil {
return data, err
}
for _, user := range equipped.GetUsers() {
if user.GetUserId() != userID {
// batch RPC 按用户分组返回;这里防御上游误返回其他用户数据。
continue
}
for _, item := range user.GetResources() {
// Resources 保留完整佩戴列表,便于客户端以后按 resource_type 自行扩展展示位。
resource := appearanceResourceFromEntitlement(item)
data.Resources = append(data.Resources, resource)
switch item.GetResource().GetResourceType() {
case appearanceResourceAvatarFrame:
data.AvatarFrame = resource
case appearanceResourceProfileCard:
data.ProfileCard = resource
case appearanceResourceVehicle:
data.Vehicle = resource
case appearanceResourceBadge:
data.EquippedBadges = append(data.EquippedBadges, resource)
}
}
}
// badge_display 是 activity-service 的投影wallet 只提供 badge 资源素材和权益事实。
badgeProfile, err := h.badgeProfileForUser(request, userID)
if err != nil {
return data, err
}
data.BadgeProfile = badgeProfile
// 顶层 badges 给高频展示位直接消费;完整 profile 仍保留在 badge_profile 中。
data.Badges = badgeProfile.StripBadges
data.ServerTimeMS = badgeProfile.ServerTimeMS
return data, nil
}
func (h *Handler) badgeProfileForUser(request *http.Request, userID int64) (appearanceBadgeData, error) {
result := appearanceBadgeData{
StripBadges: []map[string]any{},
ProfileTileBadges: []map[string]any{},
HonorBadges: []map[string]any{},
}
if h.achievementClient == nil {
// 未接 activity 时保留装扮数据返回badge 投影为空;这不会影响头像框/座驾展示。
return result, nil
}
resp, err := h.achievementClient.ListMyBadges(request.Context(), &activityv1.ListMyBadgesRequest{
Meta: httpkit.ActivityMeta(request),
UserId: userID,
})
if err != nil {
return result, err
}
// activity 的 badge item 只保存 resource_id资源图标/动画素材仍从 wallet resource 表解析。
resources, err := h.appearanceResourcesByID(request, badgeResourceIDs(resp))
if err != nil {
return result, err
}
result.StripBadges = badgeItemsFromProto(resp.GetStripBadges(), resources)
result.ProfileTileBadges = badgeItemsFromProto(resp.GetProfileTileBadges(), resources)
result.HonorBadges = badgeItemsFromProto(resp.GetHonorBadges(), resources)
result.ServerTimeMS = resp.GetServerTimeMs()
return result, nil
}
func (h *Handler) appearanceResourcesByID(request *http.Request, ids []int64) (map[int64]map[string]any, error) {
ids = uniqueAppearanceResourceIDs(ids)
resources := make(map[int64]map[string]any, len(ids))
if len(ids) == 0 {
return resources, nil
}
if h.walletClient == nil {
// 只有 badge 素材解析会走到这里;缺 wallet 表示无法把 badge_id 转成可展示资源。
return nil, errAppearanceUpstream
}
for _, id := range ids {
resp, err := h.walletClient.GetResource(request.Context(), &walletv1.GetResourceRequest{
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
ResourceId: id,
})
if err != nil {
return nil, err
}
resource := appearanceResourceFromProto(resp.GetResource(), "", 0)
if resourceID, ok := resource["resource_id"].(int64); ok && resourceID > 0 {
resources[resourceID] = resource
}
}
return resources, nil
}
func appearanceResourceFromEntitlement(item *walletv1.UserResourceEntitlement) map[string]any {
if item == nil {
return map[string]any{}
}
// entitlement_id/expires_at_ms 是佩戴态字段resource 本身不带这两个用户维度。
return appearanceResourceFromProto(item.GetResource(), item.GetEntitlementId(), item.GetExpiresAtMs())
}
func appearanceResourceFromProto(item *walletv1.Resource, entitlementID string, expiresAtMS int64) map[string]any {
if item == nil || item.GetResourceId() <= 0 {
return map[string]any{}
}
return map[string]any{
"resource_id": item.GetResourceId(),
"resource_code": item.GetResourceCode(),
"resource_type": item.GetResourceType(),
"name": item.GetName(),
"asset_url": item.GetAssetUrl(),
"preview_url": item.GetPreviewUrl(),
"animation_url": item.GetAnimationUrl(),
"metadata_json": item.GetMetadataJson(),
"entitlement_id": entitlementID,
"expires_at_ms": expiresAtMS,
}
}
func badgeItemsFromProto(items []*activityv1.BadgeDisplayItem, resources map[int64]map[string]any) []map[string]any {
result := make([]map[string]any, 0, len(items))
for _, item := range items {
if item == nil {
continue
}
// badge item 保留 activity 的展示位字段,同时在 resource 内嵌 wallet 资源素材。
data := map[string]any{
"slot": item.GetSlot(),
"position": item.GetPosition(),
"badge_form": item.GetBadgeForm(),
"resource_id": item.GetResourceId(),
"entitlement_id": item.GetEntitlementId(),
"source_type": item.GetSourceType(),
"source_id": item.GetSourceId(),
"pin_mode": item.GetPinMode(),
"updated_at_ms": item.GetUpdatedAtMs(),
}
if resource, ok := resources[item.GetResourceId()]; ok {
data["resource"] = resource
}
result = append(result, data)
}
return result
}
func badgeResourceIDs(resp *activityv1.ListMyBadgesResponse) []int64 {
if resp == nil {
return nil
}
// 三个展示区都可能引用 badge 资源;统一收集后去重解析素材。
ids := make([]int64, 0, len(resp.GetStripBadges())+len(resp.GetProfileTileBadges())+len(resp.GetHonorBadges()))
for _, item := range resp.GetStripBadges() {
ids = append(ids, item.GetResourceId())
}
for _, item := range resp.GetProfileTileBadges() {
ids = append(ids, item.GetResourceId())
}
for _, item := range resp.GetHonorBadges() {
ids = append(ids, item.GetResourceId())
}
return ids
}
func uniqueAppearanceResourceIDs(ids []int64) []int64 {
seen := make(map[int64]struct{}, len(ids))
result := make([]int64, 0, len(ids))
for _, id := range ids {
if id <= 0 {
continue
}
if _, exists := seen[id]; exists {
continue
}
seen[id] = struct{}{}
result = append(result, id)
}
return result
}

View File

@ -16,6 +16,7 @@ type Handler struct {
userCountryClient client.UserCountryQueryClient
userHostClient client.UserHostClient
walletClient client.WalletClient
achievementClient client.AchievementClient
broadcastClient client.BroadcastClient
}
@ -26,6 +27,7 @@ type Config struct {
UserCountryClient client.UserCountryQueryClient
UserHostClient client.UserHostClient
WalletClient client.WalletClient
AchievementClient client.AchievementClient
BroadcastClient client.BroadcastClient
}
@ -37,6 +39,7 @@ func New(config Config) *Handler {
userCountryClient: config.UserCountryClient,
userHostClient: config.UserHostClient,
walletClient: config.WalletClient,
achievementClient: config.AchievementClient,
broadcastClient: config.BroadcastClient,
}
}
@ -50,6 +53,11 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
GetMyRoleSummary: h.getMyRoleSummary,
SearchHostAgencies: h.searchHostAgencies,
ApplyToHostAgency: h.applyToHostAgency,
GetAgencyCenterOverview: h.getAgencyCenterOverview,
ListAgencyCenterHosts: h.listAgencyCenterHosts,
ListAgencyCenterApplications: h.listAgencyCenterApplications,
ReviewAgencyCenterApplication: h.reviewAgencyCenterApplication,
RemoveAgencyCenterHost: h.removeAgencyCenterHost,
CompleteMyOnboarding: h.completeMyOnboarding,
UpdateMyProfile: h.updateMyProfile,
ChangeMyCountry: h.changeMyCountry,
@ -60,6 +68,8 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
ListMyFriends: h.listMyFriends,
ListMyFriendApplications: h.listMyFriendApplications,
GetMyProfile: h.getMyProfile,
GetMyAppearance: h.getMyAppearance,
GetUserAppearance: h.getUserAppearance,
UserSocialAction: h.userSocialAction,
SubmitReport: h.submitReport,
}

View File

@ -14,6 +14,8 @@ import (
type WalletClient interface {
// DebitGift 执行送礼扣费;失败时 room-service 不进入 Room Cell 状态提交。
DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error)
// BatchDebitGift 在 wallet-service 单事务内结算多目标送礼;任一目标失败时整批失败。
BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error)
// GrantResourceGroup 发放宝箱奖励资源组;命令 ID 必须由 room-service 按宝箱结算事实生成。
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error)
}

View File

@ -28,6 +28,12 @@ func (c *grpcWalletClient) DebitGift(ctx context.Context, req *walletv1.DebitGif
return c.client.DebitGift(ctx, req)
}
// BatchDebitGift 直接转调 wallet-service 批量送礼接口。
func (c *grpcWalletClient) BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
// 多目标送礼必须由 wallet-service 单事务结算room-service 不能自己循环单目标扣费。
return c.client.BatchDebitGift(ctx, req)
}
// GrantResourceGroup 直接转调 wallet-service 资源组发放接口。
func (c *grpcWalletClient) GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
// 宝箱奖励以 resource group 为后台配置单位wallet-service 负责展开资产和权益。

View File

@ -120,6 +120,8 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
}
base.ActorUserID = body.GetUserId()
base.TargetUserID = body.GetUserId()
// RoomUserJoined 的座驾必须从 outbox body 取快照,不能在补偿投递时按当前佩戴重算。
base.EntryVehicle = roomEntryVehicleFromEvent(body.GetEntryVehicle())
base.Attributes = map[string]string{"role": body.GetRole()}
return base, true, nil
case "RoomUserLeft":
@ -356,6 +358,23 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
}
}
func roomEntryVehicleFromEvent(item *roomeventsv1.RoomEntryVehicleSnapshot) *tencentim.RoomEntryVehicleSnapshot {
if item == nil || item.GetResourceId() <= 0 {
return nil
}
return &tencentim.RoomEntryVehicleSnapshot{
ResourceID: item.GetResourceId(),
ResourceCode: item.GetResourceCode(),
Name: item.GetName(),
AssetURL: item.GetAssetUrl(),
PreviewURL: item.GetPreviewUrl(),
AnimationURL: item.GetAnimationUrl(),
MetadataJSON: item.GetMetadataJson(),
EntitlementID: item.GetEntitlementId(),
ExpiresAtMS: item.GetExpiresAtMs(),
}
}
func eventTypeForClient(eventType string) string {
// protobuf 事件名使用 PascalCase客户端 TIMCustomElem 使用 snake_case 作为稳定协议字段。
switch eventType {

View File

@ -32,6 +32,39 @@ func TestRoomEventFromEnvelopeSkipsNonIMEvents(t *testing.T) {
}
}
func TestRoomUserJoinedCarriesEntryVehicleSnapshot(t *testing.T) {
record, err := outbox.Build("room-vehicle", "RoomUserJoined", 9, time.Now(), &roomeventsv1.RoomUserJoined{
UserId: 42,
Role: "audience",
VisibleRegionId: 86,
EntryVehicle: &roomeventsv1.RoomEntryVehicleSnapshot{
ResourceId: 7001,
ResourceCode: "vehicle_gold",
Name: "Gold Vehicle",
AssetUrl: "https://cdn.example.com/vehicle.png",
PreviewUrl: "https://cdn.example.com/vehicle-preview.png",
AnimationUrl: "https://cdn.example.com/vehicle.svga",
MetadataJson: `{"speed":"fast"}`,
EntitlementId: "ent-vehicle",
ExpiresAtMs: 1999999999999,
},
})
if err != nil {
t.Fatalf("Build RoomUserJoined envelope failed: %v", err)
}
event, publish, err := roomEventFromEnvelope(record.Envelope)
if err != nil {
t.Fatalf("RoomUserJoined should decode: %v", err)
}
if !publish {
t.Fatal("RoomUserJoined must publish Tencent IM message")
}
if event.EntryVehicle == nil || event.EntryVehicle.ResourceID != 7001 || event.EntryVehicle.AnimationURL != "https://cdn.example.com/vehicle.svga" {
t.Fatalf("entry vehicle snapshot mismatch: %+v", event.EntryVehicle)
}
}
func TestRoomBackgroundChangedPublishesDedicatedIMEvent(t *testing.T) {
record, err := outbox.Build("room-bg", "RoomBackgroundChanged", 7, time.Now(), &roomeventsv1.RoomBackgroundChanged{
ActorUserId: 42,

View File

@ -105,10 +105,24 @@ type SetRoomBackground struct {
func (SetRoomBackground) Type() string { return "set_room_background" }
// JoinRoom 定义业务进房请求。
type EntryVehicleSnapshot struct {
ResourceID int64 `json:"resource_id,omitempty"`
ResourceCode string `json:"resource_code,omitempty"`
Name string `json:"name,omitempty"`
AssetURL string `json:"asset_url,omitempty"`
PreviewURL string `json:"preview_url,omitempty"`
AnimationURL string `json:"animation_url,omitempty"`
MetadataJSON string `json:"metadata_json,omitempty"`
EntitlementID string `json:"entitlement_id,omitempty"`
ExpiresAtMS int64 `json:"expires_at_ms,omitempty"`
}
type JoinRoom struct {
Base
// Role 是用户在房间内的轻量角色,默认 audience。
Role string `json:"role"`
// EntryVehicle 是 gateway 在进房时解析出的当前有效佩戴座驾快照,用于进房 IM。
EntryVehicle EntryVehicleSnapshot `json:"entry_vehicle,omitempty"`
}
// Type 返回命令类型。
@ -358,9 +372,9 @@ type SendGift struct {
Base
// TargetType 当前只支持 user保留字段避免后续 all_mic/all_room/couple 再改命令结构。
TargetType string `json:"target_type"`
// TargetUserIDs 是新协议目标用户集合;当前 target_type=user 时只允许一个用户。
// TargetUserIDs 是新协议目标用户集合;target_type=user 时允许多个房间内用户。
TargetUserIDs []int64 `json:"target_user_ids,omitempty"`
// TargetUserID 是礼物接收方,当前版本要求接收方在房间内。
// TargetUserID 是兼容单目标字段和批量主目标,所有接收方仍要求在房间内。
TargetUserID int64 `json:"target_user_id"`
// GiftID 是礼物配置标识,具体配置有效性当前由调用方和 wallet 链路保证。
GiftID string `json:"gift_id"`

View File

@ -20,6 +20,10 @@ func (followTestWallet) DebitGift(context.Context, *walletv1.DebitGiftRequest) (
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-follow-test"}, nil
}
func (followTestWallet) BatchDebitGift(context.Context, *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
return &walletv1.BatchDebitGiftResponse{Aggregate: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-follow-test"}}, nil
}
func (followTestWallet) GrantResourceGroup(context.Context, *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "grant-follow-test"}}, nil
}

View File

@ -3,6 +3,7 @@ package service
import (
"context"
"fmt"
"strconv"
"strings"
"time"
@ -39,7 +40,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
if cmd.TargetType == "" {
cmd.TargetType = "user"
}
if cmd.TargetType == "user" && len(cmd.TargetUserIDs) == 1 {
if cmd.TargetType == "user" && len(cmd.TargetUserIDs) > 0 {
cmd.TargetUserID = cmd.TargetUserIDs[0]
}
@ -53,18 +54,24 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
// v1 HTTP 契约已预留 all_mic/all_room/couple账务拆单和房间表现策略补齐前先 fail-close。
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_type is unsupported")
}
if len(cmd.TargetUserIDs) != 1 || cmd.TargetUserID <= 0 {
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_ids must contain one user")
if len(cmd.TargetUserIDs) == 0 || cmd.TargetUserID <= 0 {
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_ids is required")
}
if _, exists := current.OnlineUsers[cmd.TargetUserID]; !exists {
// v1 要求礼物目标在房间内,避免给不存在的房间表现目标计榜。
for _, targetUserID := range cmd.TargetUserIDs {
if _, exists := current.OnlineUsers[targetUserID]; !exists {
// 每个接收方都必须在房间 presence 内;批量扣费前先全部校验,避免钱包出现无房间表现的入账。
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not in room")
}
}
if cmd.GiftID == "" || cmd.GiftCount <= 0 {
// 礼物 ID 和数量是钱包查服务端价格的最小输入,客户端不再提交礼物单价。
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "gift_id and gift_count must be valid")
}
if len(cmd.TargetUserIDs) > 1 && cmd.PoolID != "" {
// 幸运礼物 activity 协议当前只有单个 target_user_id多目标先只开放普通礼物避免抽奖事实错绑接收方。
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "lucky gift requires one target user")
}
roomMeta, exists, err := s.repository.GetRoomMeta(ctx, cmd.RoomID())
if err != nil {
return mutationResult{}, nil, err
@ -100,6 +107,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
}
var billing *walletv1.DebitGiftResponse
var targetBillings []giftTargetBilling
var walletDebitMS int64
settledCommand := cmd
var luckyGift *roomv1.LuckyGiftDrawResult
@ -107,20 +115,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
// 钱包扣费在房间状态变更前完成;失败时不写 command log、不进入 Room Cell 提交态。
walletStartedAt := time.Now()
var err error
billing, err = s.wallet.DebitGift(ctx, &walletv1.DebitGiftRequest{
CommandId: cmd.ID(),
RoomId: cmd.RoomID(),
SenderUserId: cmd.ActorUserID(),
TargetUserId: cmd.TargetUserID,
GiftId: cmd.GiftID,
GiftCount: cmd.GiftCount,
AppCode: appcode.FromContext(ctx),
RegionId: roomMeta.VisibleRegionID,
SenderRegionId: cmd.SenderRegionID,
TargetIsHost: cmd.TargetIsHost,
TargetHostRegionId: cmd.TargetHostRegionID,
TargetAgencyOwnerUserId: cmd.TargetAgencyOwnerUserID,
})
billing, targetBillings, err = s.debitGiftTargets(ctx, cmd, roomMeta)
walletDebitMS = elapsedMS(walletStartedAt)
if err != nil {
return err
@ -136,7 +131,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
settledCommand.GiftTypeCode = billing.GetGiftTypeCode()
settledCommand.HostPeriodDiamondAdded = billing.GetHostPeriodDiamondAdded()
settledCommand.HostPeriodCycleKey = billing.GetHostPeriodCycleKey()
if !luckyEnabled {
if !luckyEnabled && len(cmd.TargetUserIDs) == 1 {
// 未显式传 pool_id 的旧客户端仍可通过钱包礼物类型触发默认幸运礼物逻辑;新客户端应传明确 pool_id。
luckyEnabled = s.shouldDrawLuckyGift(cmd.PoolID, billing.GetGiftTypeCode())
}
@ -190,21 +185,25 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
return mutationResult{}, nil, err
}
// 送礼事件、热度事件和榜单事件使用同一个房间版本,方便消费者关联同一次命令。
// 送礼事件按 target 拆分,保证 IM、统计和礼物墙消费者拿到准确的接收方和账务回执。
giftEvents := make([]outbox.Record, 0, len(targetBillings))
for _, targetBilling := range targetBillings {
giftEvent, err := outbox.Build(current.RoomID, "RoomGiftSent", current.Version, now, &roomeventsv1.RoomGiftSent{
SenderUserId: cmd.ActorUserID(),
TargetUserId: cmd.TargetUserID,
TargetUserId: targetBilling.TargetUserID,
GiftId: cmd.GiftID,
PoolId: cmd.PoolID,
GiftCount: cmd.GiftCount,
GiftValue: heatValue,
BillingReceiptId: billing.GetBillingReceiptId(),
GiftValue: targetBilling.Billing.GetHeatValue(),
BillingReceiptId: targetBilling.Billing.GetBillingReceiptId(),
VisibleRegionId: roomMeta.VisibleRegionID,
CommandId: cmd.ID(),
CommandId: targetBilling.CommandID,
})
if err != nil {
return mutationResult{}, nil, err
}
giftEvents = append(giftEvents, giftEvent)
}
heatEvent, err := outbox.Build(current.RoomID, "RoomHeatChanged", current.Version, now, &roomeventsv1.RoomHeatChanged{
Delta: heatValue,
@ -224,7 +223,9 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
return mutationResult{}, nil, err
}
records := []outbox.Record{giftEvent, heatEvent, rankEvent}
records := make([]outbox.Record, 0, len(giftEvents)+4)
records = append(records, giftEvents...)
records = append(records, heatEvent, rankEvent)
if treasureApply.progressEvent != nil {
progressEvent, err := outbox.Build(current.RoomID, "RoomTreasureProgressChanged", current.Version, now, treasureApply.progressEvent)
if err != nil {
@ -265,7 +266,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
},
syncEvent: &tencentim.RoomEvent{
// 同步广播只选 GiftSent 主事件作为客户端房间系统消息Heat/Rank 通过字段携带。
EventID: giftEvent.EventID,
EventID: giftEvents[0].EventID,
RoomID: current.RoomID,
EventType: "room_gift_sent",
ActorUserID: cmd.ActorUserID(),
@ -280,6 +281,8 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
"billing_receipt_id": billing.GetBillingReceiptId(),
"coin_spent": fmt.Sprintf("%d", settledCommand.CoinSpent),
"gift_point_added": fmt.Sprintf("%d", settledCommand.GiftPointAdded),
"target_count": fmt.Sprintf("%d", len(cmd.TargetUserIDs)),
"target_user_ids": giftTargetUserIDsAttribute(cmd.TargetUserIDs),
"price_version": settledCommand.PriceVersion,
"host_period_diamond_added": fmt.Sprintf("%d", settledCommand.HostPeriodDiamondAdded),
"host_period_cycle_key": settledCommand.HostPeriodCycleKey,
@ -302,6 +305,70 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
}, nil
}
type giftTargetBilling struct {
TargetUserID int64
CommandID string
Billing *walletv1.DebitGiftResponse
}
func (s *Service) debitGiftTargets(ctx context.Context, cmd command.SendGift, roomMeta RoomMeta) (*walletv1.DebitGiftResponse, []giftTargetBilling, error) {
if len(cmd.TargetUserIDs) == 1 {
// 单目标保留原 wallet command_id避免改变已有幂等键、账务回执和排障路径。
billing, err := s.wallet.DebitGift(ctx, &walletv1.DebitGiftRequest{
CommandId: cmd.ID(),
RoomId: cmd.RoomID(),
SenderUserId: cmd.ActorUserID(),
TargetUserId: cmd.TargetUserID,
GiftId: cmd.GiftID,
GiftCount: cmd.GiftCount,
AppCode: appcode.FromContext(ctx),
RegionId: roomMeta.VisibleRegionID,
SenderRegionId: cmd.SenderRegionID,
TargetIsHost: cmd.TargetIsHost,
TargetHostRegionId: cmd.TargetHostRegionID,
TargetAgencyOwnerUserId: cmd.TargetAgencyOwnerUserID,
})
if err != nil {
return nil, nil, err
}
return billing, []giftTargetBilling{{TargetUserID: cmd.TargetUserID, CommandID: cmd.ID(), Billing: billing}}, nil
}
targets := make([]*walletv1.DebitGiftTarget, 0, len(cmd.TargetUserIDs))
for _, targetUserID := range cmd.TargetUserIDs {
target := &walletv1.DebitGiftTarget{
CommandId: giftTargetCommandID(cmd.ID(), targetUserID, len(cmd.TargetUserIDs)),
TargetUserId: targetUserID,
}
if targetUserID == cmd.TargetUserID && cmd.TargetIsHost {
// 现有 room proto 只有单个 host scope多目标时只允许把该快照用于对应的第一个目标不能错误扩散到其他接收方。
target.TargetIsHost = true
target.TargetHostRegionId = cmd.TargetHostRegionID
target.TargetAgencyOwnerUserId = cmd.TargetAgencyOwnerUserID
}
targets = append(targets, target)
}
resp, err := s.wallet.BatchDebitGift(ctx, &walletv1.BatchDebitGiftRequest{
CommandId: cmd.ID(),
RoomId: cmd.RoomID(),
SenderUserId: cmd.ActorUserID(),
GiftId: cmd.GiftID,
GiftCount: cmd.GiftCount,
AppCode: appcode.FromContext(ctx),
RegionId: roomMeta.VisibleRegionID,
SenderRegionId: cmd.SenderRegionID,
Targets: targets,
})
if err != nil {
return nil, nil, err
}
targetBillings, err := giftTargetBillingsFromBatch(resp, cmd.TargetUserIDs)
if err != nil {
return nil, nil, err
}
return resp.GetAggregate(), targetBillings, nil
}
func roomGiftLeaderboardCoinSpent(billing *walletv1.DebitGiftResponse) int64 {
if billing == nil {
return 0
@ -419,6 +486,52 @@ func normalizeGiftTargetUserIDs(targetUserID int64, targetUserIDs []int64) []int
return ids
}
func giftTargetCommandID(commandID string, targetUserID int64, targetCount int) string {
if targetCount <= 1 {
return commandID
}
// 钱包以 command_id 做幂等主键;多目标必须按接收方派生稳定子命令,才能在同一批内保存独立交易。
return fmt.Sprintf("%s:target:%d", commandID, targetUserID)
}
func giftTargetBillingsFromBatch(resp *walletv1.BatchDebitGiftResponse, targetUserIDs []int64) ([]giftTargetBilling, error) {
if resp == nil || resp.GetAggregate() == nil {
return nil, xerr.New(xerr.Unavailable, "wallet batch debit response is empty")
}
receipts := make(map[int64]*walletv1.BatchDebitGiftReceipt, len(resp.GetReceipts()))
for _, receipt := range resp.GetReceipts() {
if receipt.GetTargetUserId() <= 0 || receipt.GetBilling() == nil {
return nil, xerr.New(xerr.Unavailable, "wallet batch debit receipt is incomplete")
}
receipts[receipt.GetTargetUserId()] = receipt
}
targetBillings := make([]giftTargetBilling, 0, len(targetUserIDs))
for _, targetUserID := range targetUserIDs {
receipt := receipts[targetUserID]
if receipt == nil {
// 批量钱包响应必须覆盖每个房间目标;少一条就不能提交房间表现和 outbox。
return nil, xerr.New(xerr.Unavailable, "wallet batch debit receipt is missing")
}
targetBillings = append(targetBillings, giftTargetBilling{
TargetUserID: targetUserID,
CommandID: receipt.GetCommandId(),
Billing: receipt.GetBilling(),
})
}
return targetBillings, nil
}
func giftTargetUserIDsAttribute(targetUserIDs []int64) string {
if len(targetUserIDs) == 0 {
return ""
}
parts := make([]string, 0, len(targetUserIDs))
for _, targetUserID := range targetUserIDs {
parts = append(parts, strconv.FormatInt(targetUserID, 10))
}
return strings.Join(parts, ",")
}
func findRankItem(items []state.RankItem, userID int64) state.RankItem {
// 送礼后通常能找到发送方榜单项;找不到时返回 user_id 占位避免空事件。
for _, item := range items {

View File

@ -27,6 +27,8 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
cmd := command.JoinRoom{
Base: baseFromMeta(req.GetMeta()),
Role: req.GetRole(),
// entry_vehicle 来自 gateway 入房瞬间的 wallet 快照,写入 command log 后可稳定重放。
EntryVehicle: entryVehicleSnapshotFromProto(req.GetEntryVehicle()),
}
if cmd.Role == "" {
@ -76,6 +78,8 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
UserId: cmd.ActorUserID(),
Role: cmd.Role,
VisibleRegionId: current.VisibleRegionID,
// outbox 事件必须带座驾快照;否则异步补偿投递 IM 时会丢失入场座驾。
EntryVehicle: eventEntryVehicleFromCommand(cmd.EntryVehicle),
})
if err != nil {
return mutationResult{}, nil, err
@ -93,6 +97,8 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
ActorUserID: cmd.ActorUserID(),
TargetUserID: cmd.ActorUserID(),
RoomVersion: current.Version,
// syncEvent 与 outbox 事件保持同一份快照,避免低延迟路径和补偿路径 payload 不一致。
EntryVehicle: imEntryVehicleFromCommand(cmd.EntryVehicle),
},
}, []outbox.Record{joinedEvent}, nil
})
@ -108,6 +114,60 @@ func (s *Service) JoinRoom(ctx context.Context, req *roomv1.JoinRoomRequest) (*r
}, nil
}
func entryVehicleSnapshotFromProto(item *roomv1.RoomEntryVehicleSnapshot) command.EntryVehicleSnapshot {
if item == nil || item.GetResourceId() <= 0 {
return command.EntryVehicleSnapshot{}
}
// 只裁剪字符串不做有效期二次判断wallet batch RPC 已经负责过期佩戴过滤。
return command.EntryVehicleSnapshot{
ResourceID: item.GetResourceId(),
ResourceCode: strings.TrimSpace(item.GetResourceCode()),
Name: strings.TrimSpace(item.GetName()),
AssetURL: strings.TrimSpace(item.GetAssetUrl()),
PreviewURL: strings.TrimSpace(item.GetPreviewUrl()),
AnimationURL: strings.TrimSpace(item.GetAnimationUrl()),
MetadataJSON: strings.TrimSpace(item.GetMetadataJson()),
EntitlementID: strings.TrimSpace(item.GetEntitlementId()),
ExpiresAtMS: item.GetExpiresAtMs(),
}
}
func eventEntryVehicleFromCommand(item command.EntryVehicleSnapshot) *roomeventsv1.RoomEntryVehicleSnapshot {
if item.ResourceID <= 0 {
return nil
}
// 事件 proto 是补偿投递、统计消费的稳定事实,字段不依赖后续资源表变更。
return &roomeventsv1.RoomEntryVehicleSnapshot{
ResourceId: item.ResourceID,
ResourceCode: item.ResourceCode,
Name: item.Name,
AssetUrl: item.AssetURL,
PreviewUrl: item.PreviewURL,
AnimationUrl: item.AnimationURL,
MetadataJson: item.MetadataJSON,
EntitlementId: item.EntitlementID,
ExpiresAtMs: item.ExpiresAtMS,
}
}
func imEntryVehicleFromCommand(item command.EntryVehicleSnapshot) *tencentim.RoomEntryVehicleSnapshot {
if item.ResourceID <= 0 {
return nil
}
// Tencent IM payload 使用客户端展示字段,保持和 /appearance 的资源字段命名一致。
return &tencentim.RoomEntryVehicleSnapshot{
ResourceID: item.ResourceID,
ResourceCode: item.ResourceCode,
Name: item.Name,
AssetURL: item.AssetURL,
PreviewURL: item.PreviewURL,
AnimationURL: item.AnimationURL,
MetadataJSON: item.MetadataJSON,
EntitlementID: item.EntitlementID,
ExpiresAtMS: item.ExpiresAtMS,
}
}
// projectRoomVisitBestEffort 记录 Mine/Visited tab 的房间访问索引。
// 这是列表读模型,不参与 Room Cell 提交;失败只记录日志,不能影响已经成功的 JoinRoom。
func (s *Service) projectRoomVisitBestEffort(ctx context.Context, userID int64, roomID string) {

View File

@ -32,10 +32,13 @@ type treasureTestWallet struct {
debits []*walletv1.DebitGiftResponse
grants []*walletv1.GrantResourceGroupRequest
lastDebit *walletv1.DebitGiftRequest
debitRequests []*walletv1.DebitGiftRequest
lastBatch *walletv1.BatchDebitGiftRequest
}
func (w *treasureTestWallet) DebitGift(_ context.Context, req *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) {
w.lastDebit = req
w.debitRequests = append(w.debitRequests, req)
if len(w.debits) == 0 {
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-default", GiftPointAdded: 1, HeatValue: 1}, nil
}
@ -44,6 +47,114 @@ func (w *treasureTestWallet) DebitGift(_ context.Context, req *walletv1.DebitGif
return next, nil
}
func (w *treasureTestWallet) BatchDebitGift(_ context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
w.lastBatch = req
receipts := make([]*walletv1.BatchDebitGiftReceipt, 0, len(req.GetTargets()))
aggregate := &walletv1.DebitGiftResponse{}
for _, target := range req.GetTargets() {
var next *walletv1.DebitGiftResponse
if len(w.debits) == 0 {
next = &walletv1.DebitGiftResponse{BillingReceiptId: fmt.Sprintf("receipt-%d", target.GetTargetUserId()), GiftPointAdded: 1, HeatValue: 1, CoinSpent: 1, ChargeAmount: 1}
} else {
next = w.debits[0]
w.debits = w.debits[1:]
}
aggregate.BillingReceiptId = joinReceiptIDs(aggregate.GetBillingReceiptId(), next.GetBillingReceiptId())
aggregate.CoinSpent += next.GetCoinSpent()
aggregate.ChargeAmount += next.GetChargeAmount()
aggregate.GiftPointAdded += next.GetGiftPointAdded()
aggregate.HeatValue += next.GetHeatValue()
aggregate.HostPeriodDiamondAdded += next.GetHostPeriodDiamondAdded()
if aggregate.PriceVersion == "" {
aggregate.PriceVersion = next.GetPriceVersion()
}
if aggregate.GiftTypeCode == "" {
aggregate.GiftTypeCode = next.GetGiftTypeCode()
}
aggregate.BalanceAfter = next.GetBalanceAfter()
receipts = append(receipts, &walletv1.BatchDebitGiftReceipt{
TargetUserId: target.GetTargetUserId(),
CommandId: target.GetCommandId(),
Billing: next,
})
}
return &walletv1.BatchDebitGiftResponse{Aggregate: aggregate, Receipts: receipts}, nil
}
func joinReceiptIDs(left string, right string) string {
if left == "" {
return right
}
if right == "" {
return left
}
return left + "," + right
}
func TestSendGiftBatchDebitsMultipleTargets(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
now := &fixedRoomTreasureClock{now: time.Date(2026, 5, 20, 12, 0, 0, 0, time.UTC)}
wallet := &treasureTestWallet{debits: []*walletv1.DebitGiftResponse{
{BillingReceiptId: "receipt-202", CoinSpent: 10, ChargeAmount: 10, GiftPointAdded: 5, HeatValue: 11, GiftTypeCode: "normal", PriceVersion: "v1", BalanceAfter: 90},
{BillingReceiptId: "receipt-303", CoinSpent: 20, ChargeAmount: 20, GiftPointAdded: 7, HeatValue: 19, GiftTypeCode: "normal", PriceVersion: "v1", BalanceAfter: 70},
}}
svc := newTreasureTestService(t, repository, wallet, now)
roomID := "room-gift-multi-target"
ownerID := int64(101)
firstTargetID := int64(202)
secondTargetID := int64(303)
createTreasureRoom(t, ctx, svc, roomID, ownerID, 9001)
joinTreasureRoom(t, ctx, svc, roomID, firstTargetID)
joinTreasureRoom(t, ctx, svc, roomID, secondTargetID)
resp, err := svc.SendGift(ctx, &roomv1.SendGiftRequest{
Meta: &roomv1.RequestMeta{
RequestId: "req-gift-multi",
CommandId: "cmd-gift-multi",
ActorUserId: ownerID,
RoomId: roomID,
AppCode: appcode.Default,
SentAtMs: now.Now().UnixMilli(),
},
TargetType: "user",
TargetUserIds: []int64{firstTargetID, secondTargetID},
GiftId: "rose",
GiftCount: 2,
})
if err != nil {
t.Fatalf("send multi-target gift failed: %v", err)
}
if resp.GetRoomHeat() != 30 || resp.GetBillingReceiptId() != "receipt-202,receipt-303" {
t.Fatalf("multi-target gift response must aggregate wallet results: %+v", resp)
}
if len(wallet.debitRequests) != 0 {
t.Fatalf("multi-target gift must use wallet BatchDebitGift, single debits=%+v", wallet.debitRequests)
}
if wallet.lastBatch == nil || len(wallet.lastBatch.GetTargets()) != 2 {
t.Fatalf("wallet batch request missing targets: %+v", wallet.lastBatch)
}
if wallet.lastBatch.GetTargets()[0].GetCommandId() != "cmd-gift-multi:target:202" || wallet.lastBatch.GetTargets()[1].GetCommandId() != "cmd-gift-multi:target:303" {
t.Fatalf("wallet batch target command ids mismatch: %+v", wallet.lastBatch.GetTargets())
}
events := roomGiftSentEvents(t, ctx, repository)
if len(events) != 2 {
t.Fatalf("multi-target gift must emit one GiftSent per target, got %d", len(events))
}
got := map[int64]*roomeventsv1.RoomGiftSent{}
for _, event := range events {
got[event.GetTargetUserId()] = event
}
if got[firstTargetID].GetBillingReceiptId() != "receipt-202" || got[firstTargetID].GetGiftValue() != 11 || got[firstTargetID].GetCommandId() != "cmd-gift-multi:target:202" {
t.Fatalf("first target GiftSent mismatch: %+v", got[firstTargetID])
}
if got[secondTargetID].GetBillingReceiptId() != "receipt-303" || got[secondTargetID].GetGiftValue() != 19 || got[secondTargetID].GetCommandId() != "cmd-gift-multi:target:303" {
t.Fatalf("second target GiftSent mismatch: %+v", got[secondTargetID])
}
}
func (w *treasureTestWallet) GrantResourceGroup(_ context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
w.grants = append(w.grants, req)
return &walletv1.ResourceGrantResponse{
@ -487,6 +598,27 @@ func treasureProgressEvents(t *testing.T, ctx context.Context, repository *mysql
return events
}
func roomGiftSentEvents(t *testing.T, ctx context.Context, repository *mysqltest.Repository) []*roomeventsv1.RoomGiftSent {
t.Helper()
records, err := repository.ListPendingOutbox(ctx, 50)
if err != nil {
t.Fatalf("list pending outbox failed: %v", err)
}
events := make([]*roomeventsv1.RoomGiftSent, 0)
for _, record := range records {
if record.EventType != "RoomGiftSent" {
continue
}
var event roomeventsv1.RoomGiftSent
if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
t.Fatalf("decode gift sent event failed: %v", err)
}
events = append(events, &event)
}
return events
}
func countTreasureCountdownEvents(t *testing.T, ctx context.Context, repository *mysqltest.Repository) int {
t.Helper()

View File

@ -30,6 +30,11 @@ func (fakeWallet) DebitGift(context.Context, *walletv1.DebitGiftRequest) (*walle
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-test"}, nil
}
// BatchDebitGift 返回固定结果,避免 transport 测试依赖 wallet-service 批量扣费实现。
func (fakeWallet) BatchDebitGift(context.Context, *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
return &walletv1.BatchDebitGiftResponse{Aggregate: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-test"}}, nil
}
// GrantResourceGroup 返回固定发放结果,避免 transport 测试依赖 wallet-service 资源组实现。
func (fakeWallet) GrantResourceGroup(context.Context, *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "grant-test"}}, nil

View File

@ -34,6 +34,7 @@ type Repository interface {
GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error)
ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error)
GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error)
GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, 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)
@ -184,6 +185,17 @@ func (s *Service) KickAgencyHost(ctx context.Context, command KickAgencyHostInpu
})
}
// GetAgency 读取 Agency 详情;调用方负责根据业务入口校验 owner 或管理员身份。
func (s *Service) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error) {
if s.repository == nil {
return hostdomain.Agency{}, xerr.New(xerr.Unavailable, "host repository is not configured")
}
if agencyID <= 0 {
return hostdomain.Agency{}, xerr.New(xerr.InvalidArgument, "agency_id is required")
}
return s.repository.GetAgency(ctx, agencyID)
}
// InviteAgency 创建邀请用户成为 Agency 拥有者的待处理邀请。
func (s *Service) InviteAgency(ctx context.Context, command InviteAgencyInput) (hostdomain.RoleInvitation, error) {
if err := s.requireWriteDependencies(); err != nil {

View File

@ -110,6 +110,11 @@ func (r *Repository) GetCoinSellerProfile(ctx context.Context, userID int64) (ho
return queryCoinSellerProfile(ctx, r.db, "WHERE user_id = ?", userID)
}
// GetAgency 读取 Agency 当前事实。
func (r *Repository) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error) {
return queryAgency(ctx, r.db, "WHERE agency_id = ?", agencyID)
}
// ListActiveCoinSellersInMyRegion 返回当前用户所在区域的启用币商列表。
// 该查询从 users.region_id 出发过滤,避免把区域条件交给 gateway 或客户端传入。
func (r *Repository) ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error) {

View File

@ -238,6 +238,20 @@ func (s *Server) GetUserRoleSummary(ctx context.Context, req *userv1.GetUserRole
return &userv1.GetUserRoleSummaryResponse{Summary: toProtoUserRoleSummary(summary)}, nil
}
// GetAgency 返回 Agency 当前事实。
func (s *Server) GetAgency(ctx context.Context, req *userv1.GetAgencyRequest) (*userv1.GetAgencyResponse, error) {
if s.hostSvc == nil {
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
}
ctx = contextWithApp(ctx, req.GetMeta())
agency, err := s.hostSvc.GetAgency(ctx, req.GetAgencyId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.GetAgencyResponse{Agency: toProtoAgency(agency)}, nil
}
// CheckBusinessCapability 返回 H5/App 业务入口可用性,具体身份规则由 host service 统一判断。
func (s *Server) CheckBusinessCapability(ctx context.Context, req *userv1.CheckBusinessCapabilityRequest) (*userv1.CheckBusinessCapabilityResponse, error) {
if s.hostSvc == nil {

View File

@ -117,6 +117,34 @@ type DebitGiftCommand struct {
TargetAgencyOwnerUserID int64
}
// DebitGiftTargetCommand 是批量送礼中单个接收方的账务语义。
type DebitGiftTargetCommand struct {
// CommandID 是单个目标交易的幂等键;同一批量送礼必须为每个目标派生独立值。
CommandID string
// TargetUserID 是本次入账 GIFT_POINT 的用户。
TargetUserID int64
// TargetIsHost 只能由 gateway 注入,批量目标之间不能共享该身份快照。
TargetIsHost bool
// TargetHostRegionID 是该目标主播身份所属区域。
TargetHostRegionID int64
// TargetAgencyOwnerUserID 是该目标主播当前代理 owner 收款快照。
TargetAgencyOwnerUserID int64
}
// BatchDebitGiftCommand 在一个钱包事务中结算同一 sender 对多个 target 的同款礼物。
type BatchDebitGiftCommand struct {
AppCode string
CommandID string
RoomID string
SenderUserID int64
GiftID string
GiftCount int32
PriceVersion string
RegionID int64
SenderRegionID int64
Targets []DebitGiftTargetCommand
}
// Receipt 是账务命令落账后的稳定回执。
type Receipt struct {
BillingReceiptID string
@ -135,6 +163,19 @@ type Receipt struct {
HostPeriodCycleKey string
}
// BatchGiftTargetReceipt 保留批量送礼里每个目标的独立交易回执。
type BatchGiftTargetReceipt struct {
TargetUserID int64
CommandID string
Receipt Receipt
}
// BatchGiftReceipt 同时返回聚合展示值和逐目标事实回执。
type BatchGiftReceipt struct {
Aggregate Receipt
Targets []BatchGiftTargetReceipt
}
// HostSalarySettlementBatchCommand 是 cron 或测试触发结算批处理的最小输入。
type HostSalarySettlementBatchCommand struct {
AppCode string

View File

@ -396,6 +396,29 @@ type EquipUserResourceCommand struct {
EntitlementID string
}
type UnequipUserResourceCommand struct {
AppCode string
UserID int64
ResourceType string
}
type UnequipUserResourceResult struct {
ResourceType string
Unequipped bool
UpdatedAtMS int64
}
type BatchGetUserEquippedResourcesQuery struct {
AppCode string
UserIDs []int64
ResourceTypes []string
}
type UserEquippedResources struct {
UserID int64
Resources []UserResourceEntitlement
}
type ListResourceGrantsQuery struct {
AppCode string
TargetUserID int64

View File

@ -199,6 +199,24 @@ func (s *Service) EquipUserResource(ctx context.Context, command resourcedomain.
return s.repository.EquipUserResource(ctx, command)
}
func (s *Service) UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error) {
if s.repository == nil {
return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
command.AppCode = appcode.Normalize(command.AppCode)
ctx = appcode.WithContext(ctx, command.AppCode)
return s.repository.UnequipUserResource(ctx, command)
}
func (s *Service) BatchGetUserEquippedResources(ctx context.Context, query resourcedomain.BatchGetUserEquippedResourcesQuery) ([]resourcedomain.UserEquippedResources, error) {
if s.repository == nil {
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
query.AppCode = appcode.Normalize(query.AppCode)
ctx = appcode.WithContext(ctx, query.AppCode)
return s.repository.BatchGetUserEquippedResources(ctx, query)
}
func (s *Service) ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error) {
if s.repository == nil {
return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")

View File

@ -19,6 +19,7 @@ const walletBadgeGrantMetric = "wallet_badge_grant"
// Repository 隔离账务存储,扣费必须由 repository 保证余额和幂等。
type Repository interface {
DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error)
BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error)
GetBalances(ctx context.Context, userID int64, assetTypes []string) ([]ledger.AssetBalance, error)
AdminCreditAsset(ctx context.Context, command ledger.AdminCreditAssetCommand) (ledger.AssetBalance, string, error)
AdminCreditCoinSellerStock(ctx context.Context, command ledger.CoinSellerStockCreditCommand) (ledger.CoinSellerStockCreditReceipt, error)
@ -76,6 +77,8 @@ type Repository interface {
GrantResourceGroup(ctx context.Context, command resourcedomain.GrantResourceGroupCommand) (resourcedomain.ResourceGrant, error)
ListUserResources(ctx context.Context, query resourcedomain.ListUserResourcesQuery) ([]resourcedomain.UserResourceEntitlement, error)
EquipUserResource(ctx context.Context, command resourcedomain.EquipUserResourceCommand) (resourcedomain.UserResourceEntitlement, error)
UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error)
BatchGetUserEquippedResources(ctx context.Context, query resourcedomain.BatchGetUserEquippedResourcesQuery) ([]resourcedomain.UserEquippedResources, error)
ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error)
ListResourceShopItems(ctx context.Context, query resourcedomain.ListResourceShopItemsQuery) ([]resourcedomain.ResourceShopItem, int64, error)
UpsertResourceShopItems(ctx context.Context, command resourcedomain.ResourceShopItemsCommand) ([]resourcedomain.ResourceShopItem, error)
@ -143,6 +146,40 @@ func (s *Service) DebitGift(ctx context.Context, command ledger.DebitGiftCommand
return s.repository.DebitGift(ctx, command)
}
// BatchDebitGift 校验多目标送礼扣费命令,并要求 repository 在同一事务中完成全部目标结算。
func (s *Service) BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error) {
if command.CommandID == "" || command.RoomID == "" || command.SenderUserID <= 0 || command.GiftID == "" {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "billing command is incomplete")
}
if command.GiftCount <= 0 {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift_count must be positive")
}
if len(command.Targets) == 0 {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift targets are required")
}
seen := make(map[int64]struct{}, len(command.Targets))
for _, target := range command.Targets {
if target.CommandID == "" || target.TargetUserID <= 0 {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift target is incomplete")
}
if _, exists := seen[target.TargetUserID]; exists {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "gift target is duplicated")
}
seen[target.TargetUserID] = struct{}{}
if target.TargetIsHost && target.TargetHostRegionID <= 0 {
// 主播工资入账必须带该目标自己的主播区域,不能复用房间区域或其他目标快照。
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "target_host_region_id is required")
}
}
if s.repository == nil {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
command.AppCode = appcode.Normalize(command.AppCode)
ctx = appcode.WithContext(ctx, command.AppCode)
return s.repository.BatchDebitGift(ctx, command)
}
// ProcessHostSalarySettlementBatch 执行主播/代理工资结算批处理;日结和半月结不清空周期钻石,月底只做逻辑清算标记。
func (s *Service) ProcessHostSalarySettlementBatch(ctx context.Context, command ledger.HostSalarySettlementBatchCommand) (ledger.HostSalarySettlementBatchResult, error) {
if s.repository == nil {

View File

@ -89,6 +89,77 @@ func TestDebitGiftUsesServerPriceAndCreditsGiftPoint(t *testing.T) {
}
}
// TestBatchDebitGiftSettlesAllTargetsAtomically 验证多目标送礼一次扣总额,并为每个接收方写独立收礼事实。
func TestBatchDebitGiftSettlesAllTargetsAtomically(t *testing.T) {
repository := mysqltest.NewRepository(t)
repository.SetBalance(10001, 100)
repository.SetGiftPrice("rose", "v9", 7, 3, 11)
svc := walletservice.New(repository)
receipt, err := svc.BatchDebitGift(context.Background(), ledger.BatchDebitGiftCommand{
CommandID: "cmd-gift-batch",
RoomID: "room-1",
SenderUserID: 10001,
GiftID: "rose",
GiftCount: 2,
PriceVersion: "v9",
SenderRegionID: 1001,
Targets: []ledger.DebitGiftTargetCommand{
{CommandID: "cmd-gift-batch:target:10002", TargetUserID: 10002},
{CommandID: "cmd-gift-batch:target:10003", TargetUserID: 10003},
},
})
if err != nil {
t.Fatalf("BatchDebitGift failed: %v", err)
}
if receipt.Aggregate.CoinSpent != 28 || receipt.Aggregate.GiftPointAdded != 12 || receipt.Aggregate.HeatValue != 44 || receipt.Aggregate.BalanceAfter != 72 || len(receipt.Targets) != 2 {
t.Fatalf("batch aggregate mismatch: %+v", receipt)
}
for _, userID := range []int64{10002, 10003} {
balances, err := svc.GetBalances(context.Background(), userID, []string{ledger.AssetGiftPoint})
if err != nil {
t.Fatalf("GetBalances target %d failed: %v", userID, err)
}
if balanceAmount(balances, ledger.AssetGiftPoint) != 6 {
t.Fatalf("target %d GIFT_POINT mismatch: %+v", userID, balances)
}
}
balances, err := svc.GetBalances(context.Background(), 10001, []string{ledger.AssetCoin})
if err != nil {
t.Fatalf("GetBalances sender failed: %v", err)
}
if balanceAmount(balances, ledger.AssetCoin) != 72 {
t.Fatalf("sender COIN balance mismatch: %+v", balances)
}
if got := repository.CountRows("wallet_transactions", "command_id IN (?, ?)", "cmd-gift-batch:target:10002", "cmd-gift-batch:target:10003"); got != 2 {
t.Fatalf("batch gift should write one transaction per target, got %d", got)
}
if got := repository.CountRows("wallet_entries", "transaction_id IN (?, ?)", receipt.Targets[0].Receipt.TransactionID, receipt.Targets[1].Receipt.TransactionID); got != 4 {
t.Fatalf("batch gift should write sender and target entries per target, got %d", got)
}
again, err := svc.BatchDebitGift(context.Background(), ledger.BatchDebitGiftCommand{
CommandID: "cmd-gift-batch",
RoomID: "room-1",
SenderUserID: 10001,
GiftID: "rose",
GiftCount: 2,
PriceVersion: "v9",
SenderRegionID: 1001,
Targets: []ledger.DebitGiftTargetCommand{
{CommandID: "cmd-gift-batch:target:10002", TargetUserID: 10002},
{CommandID: "cmd-gift-batch:target:10003", TargetUserID: 10003},
},
})
if err != nil {
t.Fatalf("BatchDebitGift replay failed: %v", err)
}
if again.Aggregate.CoinSpent != receipt.Aggregate.CoinSpent || again.Targets[0].Receipt.TransactionID != receipt.Targets[0].Receipt.TransactionID || repository.CountRows("wallet_transactions", "command_id IN (?, ?)", "cmd-gift-batch:target:10002", "cmd-gift-batch:target:10003") != 2 {
t.Fatalf("batch idempotency mismatch: first=%+v again=%+v", receipt, again)
}
}
// TestDebitGiftCreditsHostPeriodDiamondsOnlyForHost 验证只有 gateway 确认的 active 主播会增加工资周期钻石。
func TestDebitGiftCreditsHostPeriodDiamondsOnlyForHost(t *testing.T) {
repository := mysqltest.NewRepository(t)
@ -2465,6 +2536,86 @@ func TestEquipUserResourceRequiresActiveEquipableEntitlement(t *testing.T) {
if got := repository.CountRows("user_resource_equipment", "user_id = ? AND resource_type = ?", int64(43001), resourcedomain.TypeBadge); got != 1 {
t.Fatalf("equipment should keep one active badge, got %d", got)
}
equippedResources, err := svc.BatchGetUserEquippedResources(ctx, resourcedomain.BatchGetUserEquippedResourcesQuery{
UserIDs: []int64{43001},
ResourceTypes: []string{resourcedomain.TypeBadge},
})
if err != nil {
t.Fatalf("batch get equipped resources failed: %v", err)
}
if len(equippedResources) != 1 || len(equippedResources[0].Resources) != 1 || equippedResources[0].Resources[0].EntitlementID != equipped.EntitlementID {
t.Fatalf("batch equipped resources mismatch: %+v", equippedResources)
}
unequipped, err := svc.UnequipUserResource(ctx, resourcedomain.UnequipUserResourceCommand{
UserID: 43001,
ResourceType: resourcedomain.TypeBadge,
})
if err != nil {
t.Fatalf("unequip badge resource failed: %v", err)
}
if !unequipped.Unequipped || unequipped.ResourceType != resourcedomain.TypeBadge {
t.Fatalf("unequip result mismatch: %+v", unequipped)
}
if got := repository.CountRows("user_resource_equipment", "user_id = ? AND resource_type = ?", int64(43001), resourcedomain.TypeBadge); got != 0 {
t.Fatalf("equipment should be removed after unequip, got %d", got)
}
emptyUnequip, err := svc.UnequipUserResource(ctx, resourcedomain.UnequipUserResourceCommand{
UserID: 43001,
ResourceType: resourcedomain.TypeBadge,
})
if err != nil {
t.Fatalf("repeat unequip badge resource failed: %v", err)
}
if emptyUnequip.Unequipped {
t.Fatalf("repeat unequip should be idempotent false: %+v", emptyUnequip)
}
vehicleResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
ResourceCode: "vehicle_expiring",
ResourceType: resourcedomain.TypeVehicle,
Name: "Expiring Vehicle",
Status: resourcedomain.StatusActive,
Grantable: true,
GrantStrategy: resourcedomain.GrantStrategySetActiveFlag,
UsageScopes: []string{"room_entry"},
OperatorUserID: 90001,
})
if err != nil {
t.Fatalf("create vehicle resource failed: %v", err)
}
vehicleGrant, err := svc.GrantResource(ctx, resourcedomain.GrantResourceCommand{
CommandID: "cmd-grant-expiring-vehicle",
TargetUserID: 43001,
ResourceID: vehicleResource.ResourceID,
Quantity: 1,
DurationMS: int64(time.Hour / time.Millisecond),
Reason: "test expired equipment",
OperatorUserID: 90001,
GrantSource: resourcedomain.GrantSourceAdmin,
})
if err != nil {
t.Fatalf("grant vehicle resource failed: %v", err)
}
if _, err := svc.EquipUserResource(ctx, resourcedomain.EquipUserResourceCommand{
UserID: 43001,
ResourceID: vehicleResource.ResourceID,
}); err != nil {
t.Fatalf("equip vehicle resource failed: %v", err)
}
repository.ExpireEntitlement(vehicleGrant.Items[0].EntitlementID, time.Now().Add(-time.Second).UnixMilli())
expiredEquipped, err := svc.BatchGetUserEquippedResources(ctx, resourcedomain.BatchGetUserEquippedResourcesQuery{
UserIDs: []int64{43001},
ResourceTypes: []string{resourcedomain.TypeVehicle},
})
if err != nil {
t.Fatalf("batch get expired equipped resources failed: %v", err)
}
if len(expiredEquipped) != 1 || len(expiredEquipped[0].Resources) != 0 {
t.Fatalf("expired equipment should be filtered: %+v", expiredEquipped)
}
if got := repository.CountRows("user_resource_equipment", "user_id = ? AND resource_type = ?", int64(43001), resourcedomain.TypeVehicle); got != 0 {
t.Fatalf("expired equipment should be pruned, got %d", got)
}
giftResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
ResourceCode: "gift_not_equipable",

View File

@ -322,6 +322,257 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
return receiptFromGiftMetadata(transactionID, metadata), nil
}
// BatchDebitGift 在一个 MySQL 事务内完成多目标送礼;任一目标失败时整批回滚,避免房间命令和账务出现部分成功。
func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDebitGiftCommand) (ledger.BatchGiftReceipt, error) {
if r == nil || r.db == nil {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
ctx = contextWithCommandApp(ctx, command.AppCode)
command.AppCode = appcode.FromContext(ctx)
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
defer func() {
// Commit 成功后 Rollback 会返回 sql.ErrTxDone忽略它可保证所有早退路径释放事务。
_ = tx.Rollback()
}()
nowMs := time.Now().UnixMilli()
if command.RegionID < 0 {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "region_id is invalid")
}
if command.SenderRegionID < 0 {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InvalidArgument, "sender_region_id is invalid")
}
giftConfig, err := r.resolveActiveGiftConfig(ctx, tx, command.GiftID, command.RegionID)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
price, err := r.resolveGiftPrice(ctx, tx, command.GiftID, command.PriceVersion, nowMs)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
chargeAmount, err := checkedMul(price.CoinPrice, int64(command.GiftCount))
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
giftPointAdded, err := checkedMul(price.GiftPointAmount, int64(command.GiftCount))
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
heatValue, err := checkedMul(price.HeatValue, int64(command.GiftCount))
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
totalChargeAmount, err := checkedMul(chargeAmount, int64(len(command.Targets)))
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
// 批量命令的每个目标都有独立 wallet command_id幂等重试必须是“全部已存在”或“全部未存在”不接受半批历史。
existing := make(map[string]ledger.BatchGiftTargetReceipt, len(command.Targets))
for _, target := range command.Targets {
targetCommand := debitGiftCommandFromBatchTarget(command, target)
requestHash := debitRequestHash(targetCommand)
txRow, exists, err := r.lookupTransaction(ctx, tx, target.CommandID, requestHash, bizTypeGiftDebit)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
if !exists {
continue
}
receipt, err := r.receiptForGiftTransaction(ctx, tx, txRow.TransactionID)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
existing[target.CommandID] = ledger.BatchGiftTargetReceipt{
TargetUserID: target.TargetUserID,
CommandID: target.CommandID,
Receipt: receipt,
}
}
if len(existing) > 0 {
if len(existing) != len(command.Targets) {
// 同一个 room 命令不应该只存在部分 wallet 子交易;直接 fail-close等待人工核对账本。
return ledger.BatchGiftReceipt{}, xerr.New(xerr.IdempotencyConflict, "wallet batch command idempotency conflict")
}
targetReceipts := make([]ledger.BatchGiftTargetReceipt, 0, len(command.Targets))
for _, target := range command.Targets {
targetReceipts = append(targetReceipts, existing[target.CommandID])
}
aggregate, err := aggregateGiftReceipts(targetReceipts)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
return ledger.BatchGiftReceipt{Aggregate: aggregate, Targets: targetReceipts}, nil
}
sender, err := r.lockAccount(ctx, tx, command.SenderUserID, price.ChargeAssetType, totalChargeAmount == 0, nowMs)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
if sender.AvailableAmount < totalChargeAmount {
return ledger.BatchGiftReceipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
}
targetAccounts, err := r.lockGiftTargetAccounts(ctx, tx, command.Targets, nowMs)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
anyHostTarget := false
for _, target := range command.Targets {
if target.TargetIsHost {
anyHostTarget = true
break
}
}
hostRatio := giftDiamondRatioSnapshot{Percent: "0.00", PPM: 0}
hostRatioRegionID := int64(0)
if anyHostTarget {
hostRatio, hostRatioRegionID, err = r.resolveGiftDiamondRatio(ctx, tx, command.AppCode, command.SenderRegionID, giftConfig.GiftTypeCode)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
}
events := make([]walletOutboxEvent, 0, len(command.Targets)*4)
targetReceipts := make([]ledger.BatchGiftTargetReceipt, 0, len(command.Targets))
for _, target := range command.Targets {
hostPeriodDiamondAdded := int64(0)
hostPeriodCycleKey := ""
giftDiamondRatioPercent := "0.00"
giftDiamondRatioRegionID := int64(0)
if target.TargetIsHost {
// 每个目标只使用自己的 host 快照;不能把某个主播的 agency/region 套给批量里的其他用户。
hostPeriodDiamondAdded, err = giftDiamondAmount(chargeAmount, hostRatio.PPM)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
giftDiamondRatioPercent = hostRatio.Percent
giftDiamondRatioRegionID = hostRatioRegionID
hostPeriodCycleKey = hostPeriodCycleKeyFromMS(nowMs)
}
transactionID := transactionID(command.AppCode, target.CommandID)
senderAfter := sender.AvailableAmount - chargeAmount
metadata := giftMetadata{
AppCode: command.AppCode,
GiftID: command.GiftID,
GiftName: giftConfig.Name,
ResourceID: giftConfig.ResourceID,
ResourceSnapshot: mustJSON(giftConfig.Resource),
GiftTypeCode: giftConfig.GiftTypeCode,
PresentationJSON: giftConfig.PresentationJSON,
SortOrder: giftConfig.SortOrder,
GiftCount: command.GiftCount,
PriceVersion: price.PriceVersion,
ChargeAssetType: price.ChargeAssetType,
ChargeAmount: chargeAmount,
CoinSpent: chargeAmount,
GiftPointAdded: giftPointAdded,
HeatValue: heatValue,
BalanceAfter: senderAfter,
BillingReceipt: billingReceiptID(command.AppCode, target.CommandID),
SenderUserID: command.SenderUserID,
SenderRegionID: command.SenderRegionID,
TargetUserID: target.TargetUserID,
TargetIsHost: target.TargetIsHost,
TargetHostRegionID: target.TargetHostRegionID,
TargetAgencyOwnerUserID: target.TargetAgencyOwnerUserID,
HostPeriodDiamondAdded: hostPeriodDiamondAdded,
GiftDiamondRatioPercent: giftDiamondRatioPercent,
GiftDiamondRatioRegionID: giftDiamondRatioRegionID,
HostPeriodCycleKey: hostPeriodCycleKey,
RoomID: command.RoomID,
CoinPrice: price.CoinPrice,
GiftPointAmount: price.GiftPointAmount,
HeatUnitValue: price.HeatValue,
}
if err := r.insertTransaction(ctx, tx, transactionID, target.CommandID, bizTypeGiftDebit, debitRequestHash(debitGiftCommandFromBatchTarget(command, target)), fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil {
return ledger.BatchGiftReceipt{}, err
}
if err := r.applyAccountDelta(ctx, tx, sender, -chargeAmount, 0, nowMs); err != nil {
return ledger.BatchGiftReceipt{}, err
}
events = append(events, balanceChangedEvent(transactionID, target.CommandID, command.SenderUserID, price.ChargeAssetType, -chargeAmount, 0, senderAfter, sender.FrozenAmount, sender.Version+1, metadata, nowMs))
sender.AvailableAmount = senderAfter
sender.Version++
targetAccount := targetAccounts[target.TargetUserID]
if err := r.applyAccountDelta(ctx, tx, targetAccount, giftPointAdded, 0, nowMs); err != nil {
return ledger.BatchGiftReceipt{}, err
}
targetAfter := targetAccount.AvailableAmount + giftPointAdded
events = append(events, balanceChangedEvent(transactionID, target.CommandID, target.TargetUserID, ledger.AssetGiftPoint, giftPointAdded, 0, targetAfter, targetAccount.FrozenAmount, targetAccount.Version+1, metadata, nowMs))
targetAccount.AvailableAmount = targetAfter
targetAccount.Version++
targetAccounts[target.TargetUserID] = targetAccount
if err := r.insertEntry(ctx, tx, walletEntry{
TransactionID: transactionID,
UserID: command.SenderUserID,
AssetType: price.ChargeAssetType,
AvailableDelta: -chargeAmount,
FrozenDelta: 0,
AvailableAfter: senderAfter,
FrozenAfter: sender.FrozenAmount,
CounterpartyUserID: target.TargetUserID,
RoomID: command.RoomID,
CreatedAtMS: nowMs,
}); err != nil {
return ledger.BatchGiftReceipt{}, err
}
if err := r.insertEntry(ctx, tx, walletEntry{
TransactionID: transactionID,
UserID: target.TargetUserID,
AssetType: ledger.AssetGiftPoint,
AvailableDelta: giftPointAdded,
FrozenDelta: 0,
AvailableAfter: targetAfter,
FrozenAfter: targetAccount.FrozenAmount,
CounterpartyUserID: command.SenderUserID,
RoomID: command.RoomID,
CreatedAtMS: nowMs,
}); err != nil {
return ledger.BatchGiftReceipt{}, err
}
if hostPeriodDiamondAdded > 0 {
hostPeriodDiamondAfter, hostPeriodDiamondVersion, err := r.creditHostPeriodDiamonds(ctx, tx, transactionID, target.CommandID, metadata, nowMs)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
metadata.HostPeriodDiamondAfter = hostPeriodDiamondAfter
metadata.HostPeriodDiamondVersion = hostPeriodDiamondVersion
}
events = append(events, giftDebitedEvent(transactionID, target.CommandID, command.SenderUserID, price.ChargeAssetType, -chargeAmount, 0, metadata, nowMs))
if hostPeriodDiamondAdded > 0 {
events = append(events, hostPeriodDiamondCreditedEvent(transactionID, target.CommandID, metadata, nowMs))
}
targetReceipts = append(targetReceipts, ledger.BatchGiftTargetReceipt{
TargetUserID: target.TargetUserID,
CommandID: target.CommandID,
Receipt: receiptFromGiftMetadata(transactionID, metadata),
})
}
if err := r.insertWalletOutbox(ctx, tx, events); err != nil {
return ledger.BatchGiftReceipt{}, err
}
if err := tx.Commit(); err != nil {
return ledger.BatchGiftReceipt{}, err
}
aggregate, err := aggregateGiftReceipts(targetReceipts)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
return ledger.BatchGiftReceipt{Aggregate: aggregate, Targets: targetReceipts}, nil
}
// GetBalances 读取用户多资产余额;指定资产不存在时返回 0 投影,便于客户端稳定渲染。
func (r *Repository) GetBalances(ctx context.Context, userID int64, assetTypes []string) ([]ledger.AssetBalance, error) {
if r == nil || r.db == nil {
@ -2434,6 +2685,100 @@ func checkedAdd(left int64, right int64) (int64, error) {
return left + right, nil
}
func debitGiftCommandFromBatchTarget(command ledger.BatchDebitGiftCommand, target ledger.DebitGiftTargetCommand) ledger.DebitGiftCommand {
// 批量目标最终仍落成独立 gift_debit 交易;复用单目标 hash 可以保证账务语义变化时照样触发幂等冲突。
return ledger.DebitGiftCommand{
AppCode: command.AppCode,
CommandID: target.CommandID,
RoomID: command.RoomID,
SenderUserID: command.SenderUserID,
TargetUserID: target.TargetUserID,
GiftID: command.GiftID,
GiftCount: command.GiftCount,
PriceVersion: command.PriceVersion,
RegionID: command.RegionID,
SenderRegionID: command.SenderRegionID,
TargetIsHost: target.TargetIsHost,
TargetHostRegionID: target.TargetHostRegionID,
TargetAgencyOwnerUserID: target.TargetAgencyOwnerUserID,
}
}
func (r *Repository) lockGiftTargetAccounts(ctx context.Context, tx *sql.Tx, targets []ledger.DebitGiftTargetCommand, nowMs int64) (map[int64]walletAccount, error) {
ids := make([]int64, 0, len(targets))
seen := make(map[int64]struct{}, len(targets))
for _, target := range targets {
if _, exists := seen[target.TargetUserID]; exists {
continue
}
seen[target.TargetUserID] = struct{}{}
ids = append(ids, target.TargetUserID)
}
sort.Slice(ids, func(left, right int) bool {
return ids[left] < ids[right]
})
accounts := make(map[int64]walletAccount, len(ids))
for _, targetUserID := range ids {
// 固定按 user_id 升序锁目标账户,降低并发批量送礼互相等待时形成死锁的概率。
account, err := r.lockAccount(ctx, tx, targetUserID, ledger.AssetGiftPoint, true, nowMs)
if err != nil {
return nil, err
}
accounts[targetUserID] = account
}
return accounts, nil
}
func aggregateGiftReceipts(targets []ledger.BatchGiftTargetReceipt) (ledger.Receipt, error) {
aggregate := ledger.Receipt{}
billingReceiptIDs := make([]string, 0, len(targets))
transactionIDs := make([]string, 0, len(targets))
for _, target := range targets {
receipt := target.Receipt
if receipt.BillingReceiptID != "" {
billingReceiptIDs = append(billingReceiptIDs, receipt.BillingReceiptID)
}
if receipt.TransactionID != "" {
transactionIDs = append(transactionIDs, receipt.TransactionID)
}
var err error
if aggregate.CoinSpent, err = checkedAdd(aggregate.CoinSpent, receipt.CoinSpent); err != nil {
return ledger.Receipt{}, err
}
if aggregate.ChargeAmount, err = checkedAdd(aggregate.ChargeAmount, receipt.ChargeAmount); err != nil {
return ledger.Receipt{}, err
}
if aggregate.GiftPointAdded, err = checkedAdd(aggregate.GiftPointAdded, receipt.GiftPointAdded); err != nil {
return ledger.Receipt{}, err
}
if aggregate.HeatValue, err = checkedAdd(aggregate.HeatValue, receipt.HeatValue); err != nil {
return ledger.Receipt{}, err
}
if aggregate.HostPeriodDiamondAdded, err = checkedAdd(aggregate.HostPeriodDiamondAdded, receipt.HostPeriodDiamondAdded); err != nil {
return ledger.Receipt{}, err
}
if aggregate.ChargeAssetType == "" {
aggregate.ChargeAssetType = receipt.ChargeAssetType
} else if receipt.ChargeAssetType != "" && aggregate.ChargeAssetType != receipt.ChargeAssetType {
aggregate.ChargeAssetType = giftWallChargeAssetMixed
}
if aggregate.GiftTypeCode == "" {
aggregate.GiftTypeCode = receipt.GiftTypeCode
}
if aggregate.PriceVersion == "" {
aggregate.PriceVersion = receipt.PriceVersion
}
if aggregate.HostPeriodCycleKey == "" {
aggregate.HostPeriodCycleKey = receipt.HostPeriodCycleKey
}
// sender 账后余额随目标顺序逐笔递减;聚合响应返回最后一笔后的余额,表示整批完成后的余额。
aggregate.BalanceAfter = receipt.BalanceAfter
}
aggregate.BillingReceiptID = strings.Join(billingReceiptIDs, ",")
aggregate.TransactionID = strings.Join(transactionIDs, ",")
return aggregate, nil
}
func hostPeriodCycleKeyFromMS(ms int64) string {
// 工资周期用 UTC 月份锚定,避免服务节点本地时区差异导致月底礼物落到不同周期。
return time.UnixMilli(ms).UTC().Format("2006-01")

View File

@ -814,6 +814,10 @@ func (r *Repository) ListUserResources(ctx context.Context, query resourcedomain
if query.UserID <= 0 {
return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
}
nowMs := time.Now().UnixMilli()
// equipment 表只记录“当前佩戴指针”,真正的有效期仍以 entitlement 为准。
// 所以背包列表读取前先清理当前用户已失效的佩戴指针,避免过期道具继续显示 equipped=true。
pruneResourceTypes := []string{}
where := `WHERE e.app_code = ? AND e.user_id = ?`
args := []any{appcode.FromContext(ctx), query.UserID}
if strings.TrimSpace(query.ResourceType) != "" {
@ -821,11 +825,18 @@ func (r *Repository) ListUserResources(ctx context.Context, query resourcedomain
if !resourcedomain.ValidResourceType(resourceType) {
return nil, xerr.New(xerr.InvalidArgument, "resource_type is invalid")
}
// 只有可佩戴资源才可能在 equipment 表中留下指针;礼物/金币等非佩戴资源不参与清理。
if isEquipableResourceType(resourceType) {
pruneResourceTypes = []string{resourceType}
}
where += ` AND r.resource_type = ?`
args = append(args, resourceType)
}
if err := r.pruneExpiredUserResourceEquipment(ctx, []int64{query.UserID}, pruneResourceTypes, nowMs); err != nil {
return nil, err
}
if query.ActiveOnly {
nowMs := time.Now().UnixMilli()
// App 背包只需要当前可用权益;后台或排障读取可关闭 ActiveOnly 看历史/过期记录。
where += ` AND e.status = 'active' AND e.effective_at_ms <= ? AND (e.expires_at_ms = 0 OR e.expires_at_ms > ?) AND e.remaining_quantity > 0`
args = append(args, nowMs, nowMs)
}
@ -904,6 +915,147 @@ func (r *Repository) EquipUserResource(ctx context.Context, command resourcedoma
return entitlement, nil
}
func (r *Repository) UnequipUserResource(ctx context.Context, command resourcedomain.UnequipUserResourceCommand) (resourcedomain.UnequipUserResourceResult, error) {
if r == nil || r.db == nil {
return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
ctx = contextWithCommandApp(ctx, command.AppCode)
command.AppCode = appcode.FromContext(ctx)
command.ResourceType = resourcedomain.NormalizeResourceType(command.ResourceType)
if command.UserID <= 0 || command.ResourceType == "" {
return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.InvalidArgument, "user_id and resource_type are required")
}
if !resourcedomain.ValidResourceType(command.ResourceType) || !isEquipableResourceType(command.ResourceType) {
return resourcedomain.UnequipUserResourceResult{}, xerr.New(xerr.InvalidArgument, "resource_type cannot be equipped")
}
// 取消佩戴要和 outbox 事件在同一事务内提交:
// UI 投影可以监听 UserResourceChanged但不能在 equipment 删除失败时收到“已卸下”的假事件。
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return resourcedomain.UnequipUserResourceResult{}, err
}
defer func() { _ = tx.Rollback() }()
nowMs := time.Now().UnixMilli()
result, err := tx.ExecContext(ctx, `
DELETE FROM user_resource_equipment
WHERE app_code = ? AND user_id = ? AND resource_type = ?`,
command.AppCode, command.UserID, command.ResourceType,
)
if err != nil {
return resourcedomain.UnequipUserResourceResult{}, err
}
// RowsAffected=0 表示用户本来就没佩戴该类型;这个场景按幂等取消处理,不抛业务错误。
affected, err := result.RowsAffected()
if err != nil {
return resourcedomain.UnequipUserResourceResult{}, err
}
if affected > 0 {
// resource_id 对“取消某一类佩戴”没有单一值payload 用 resource_type 表示变更维度。
eventCommandID := fmt.Sprintf("unequip:%d:%s:%d", command.UserID, command.ResourceType, time.Now().UnixNano())
if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
resourceOutboxEvent("UserResourceChanged", eventCommandID, command.UserID, 0, map[string]any{
"action": "unequip",
"app_code": command.AppCode,
"user_id": command.UserID,
"resource_type": command.ResourceType,
"updated_at_ms": nowMs,
"event_command": eventCommandID,
}, nowMs),
}); err != nil {
return resourcedomain.UnequipUserResourceResult{}, err
}
}
if err := tx.Commit(); err != nil {
return resourcedomain.UnequipUserResourceResult{}, err
}
return resourcedomain.UnequipUserResourceResult{
ResourceType: command.ResourceType,
Unequipped: affected > 0,
UpdatedAtMS: nowMs,
}, nil
}
func (r *Repository) BatchGetUserEquippedResources(ctx context.Context, query resourcedomain.BatchGetUserEquippedResourcesQuery) ([]resourcedomain.UserEquippedResources, error) {
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
ctx = contextWithCommandApp(ctx, query.AppCode)
userIDs := compactPositiveInt64s(query.UserIDs)
if len(userIDs) == 0 {
return []resourcedomain.UserEquippedResources{}, nil
}
// 批量查询只允许可佩戴类型,避免把背包里不可展示为装扮的资源混进房间资料。
resourceTypes, err := normalizeEquipableResourceTypes(query.ResourceTypes)
if err != nil {
return nil, err
}
nowMs := time.Now().UnixMilli()
// 先清理再查保证调用方不需要理解“equipment 只是指针”的存储细节。
if err := r.pruneExpiredUserResourceEquipment(ctx, userIDs, resourceTypes, nowMs); err != nil {
return nil, err
}
args := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(userIDs)...)
args = append(args, nowMs, nowMs)
typeFilter := ""
if len(resourceTypes) > 0 {
// resourceTypes 为空表示查询所有可佩戴类型;非空时只返回调用方关心的展示维度。
typeFilter = ` AND eq.resource_type IN (` + placeholders(len(resourceTypes)) + `)`
args = append(args, stringAnyArgs(resourceTypes)...)
}
// 从 equipment 反查 entitlement/resource而不是从 entitlement 扫描 equipped 标记:
// 这样每个 user_id + resource_type 只会返回当前佩戴的那一个有效权益。
rows, err := r.db.QueryContext(ctx, `
SELECT e.app_code, e.entitlement_id, e.user_id, e.resource_id, e.status,
e.quantity, e.remaining_quantity, e.effective_at_ms, e.expires_at_ms,
e.source_grant_id, e.created_at_ms, e.updated_at_ms,
TRUE AS equipped,
`+resourceColumnsWithAlias("r")+`
FROM user_resource_equipment eq
JOIN user_resource_entitlements e ON e.app_code = eq.app_code
AND e.user_id = eq.user_id
AND e.resource_id = eq.resource_id
AND e.entitlement_id = eq.entitlement_id
JOIN resources r ON r.app_code = e.app_code AND r.resource_id = e.resource_id
WHERE eq.app_code = ? AND eq.user_id IN (`+placeholders(len(userIDs))+`)
AND e.status = 'active'
AND e.effective_at_ms <= ? AND (e.expires_at_ms = 0 OR e.expires_at_ms > ?)
AND e.remaining_quantity > 0
AND r.status = 'active'`+typeFilter+`
ORDER BY eq.user_id ASC, eq.updated_at_ms DESC`,
args...,
)
if err != nil {
return nil, err
}
defer rows.Close()
grouped := make(map[int64][]resourcedomain.UserResourceEntitlement, len(userIDs))
for rows.Next() {
item, err := scanUserResourceEntitlement(rows)
if err != nil {
return nil, err
}
item.Equipped = true
grouped[item.UserID] = append(grouped[item.UserID], item)
}
if err := rows.Err(); err != nil {
return nil, err
}
// 返回顺序按请求 user_ids 保持稳定,调用方可以直接和入参用户列表对齐。
result := make([]resourcedomain.UserEquippedResources, 0, len(userIDs))
for _, userID := range userIDs {
result = append(result, resourcedomain.UserEquippedResources{
UserID: userID,
Resources: grouped[userID],
})
}
return result, nil
}
func (r *Repository) ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error) {
if r == nil || r.db == nil {
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
@ -2742,6 +2894,14 @@ func int64AnyArgs(values []int64) []any {
return args
}
func stringAnyArgs(values []string) []any {
args := make([]any, 0, len(values))
for _, value := range values {
args = append(args, value)
}
return args
}
func compactPositiveInt64s(values []int64) []int64 {
seen := make(map[int64]struct{}, len(values))
out := make([]int64, 0, len(values))
@ -2758,6 +2918,72 @@ func compactPositiveInt64s(values []int64) []int64 {
return out
}
func normalizeEquipableResourceTypes(values []string) ([]string, error) {
seen := make(map[string]struct{}, len(values))
out := make([]string, 0, len(values))
for _, value := range values {
resourceType := resourcedomain.NormalizeResourceType(value)
if resourceType == "" {
// 空类型不限制查询范围,保持 batch API 可以表达“查全部可佩戴资源”。
continue
}
if !resourcedomain.ValidResourceType(resourceType) || !isEquipableResourceType(resourceType) {
return nil, xerr.New(xerr.InvalidArgument, "resource_type cannot be equipped")
}
if _, exists := seen[resourceType]; exists {
// 去重后拼 IN (...),避免无意义扩大 SQL 参数和破坏调用方顺序稳定性。
continue
}
seen[resourceType] = struct{}{}
out = append(out, resourceType)
}
return out, nil
}
func (r *Repository) pruneExpiredUserResourceEquipment(ctx context.Context, userIDs []int64, resourceTypes []string, nowMs int64) error {
userIDs = compactPositiveInt64s(userIDs)
if r == nil || r.db == nil || len(userIDs) == 0 {
return nil
}
// 清理函数和查询函数复用同一套类型校验,避免查询拒绝的类型被清理 SQL 接受。
resourceTypes, err := normalizeEquipableResourceTypes(resourceTypes)
if err != nil {
return err
}
args := append([]any{appcode.FromContext(ctx)}, int64AnyArgs(userIDs)...)
args = append(args, nowMs, nowMs)
typeFilter := ""
if len(resourceTypes) > 0 {
typeFilter = ` AND eq.resource_type IN (` + placeholders(len(resourceTypes)) + `)`
args = append(args, stringAnyArgs(resourceTypes)...)
}
// 删除条件覆盖三类 stale 指针:
// 1. entitlement/resource 被删或不可用;
// 2. entitlement 未生效、过期、剩余次数耗尽;
// 3. resource 被下架。查询端随后只会看到仍可展示的佩戴。
_, err = r.db.ExecContext(ctx, `
DELETE eq
FROM user_resource_equipment eq
LEFT JOIN user_resource_entitlements e ON e.app_code = eq.app_code
AND e.user_id = eq.user_id
AND e.resource_id = eq.resource_id
AND e.entitlement_id = eq.entitlement_id
LEFT JOIN resources r ON r.app_code = eq.app_code AND r.resource_id = eq.resource_id
WHERE eq.app_code = ? AND eq.user_id IN (`+placeholders(len(userIDs))+`)
AND (
e.entitlement_id IS NULL
OR e.status <> 'active'
OR e.effective_at_ms > ?
OR (e.expires_at_ms <> 0 AND e.expires_at_ms <= ?)
OR e.remaining_quantity <= 0
OR r.resource_id IS NULL
OR r.status <> 'active'
)`+typeFilter,
args...,
)
return err
}
func mapResourceWriteError(err error) error {
if isMySQLDuplicateError(err) {
return xerr.New(xerr.Conflict, "resource_code already exists")

View File

@ -61,6 +61,20 @@ func (r *Repository) ActiveResourceEntitlement(userID int64, resourceID int64) (
return expiresAtMS, quantity, remainingQuantity
}
// ExpireEntitlement moves a granted entitlement into the past for expiry-path assertions.
func (r *Repository) ExpireEntitlement(entitlementID string, expiresAtMS int64) {
r.t.Helper()
if _, err := r.schema.DB.ExecContext(context.Background(), `
UPDATE user_resource_entitlements
SET expires_at_ms = ?, updated_at_ms = ?
WHERE entitlement_id = ?`,
expiresAtMS, time.Now().UnixMilli(), entitlementID,
); err != nil {
r.t.Fatalf("expire entitlement failed: %v", err)
}
}
// HostSalarySettlementIDs returns the persisted settlement IDs for one host/cycle in creation order.
func (r *Repository) HostSalarySettlementIDs(userID int64, cycleKey string) []string {
r.t.Helper()

View File

@ -269,6 +269,45 @@ func (s *Server) EquipUserResource(ctx context.Context, req *walletv1.EquipUserR
return &walletv1.EquipUserResourceResponse{Resource: userResourceToProto(item)}, nil
}
func (s *Server) UnequipUserResource(ctx context.Context, req *walletv1.UnequipUserResourceRequest) (*walletv1.UnequipUserResourceResponse, error) {
result, err := s.svc.UnequipUserResource(ctx, resourcedomain.UnequipUserResourceCommand{
AppCode: req.GetAppCode(),
UserID: req.GetUserId(),
ResourceType: req.GetResourceType(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &walletv1.UnequipUserResourceResponse{
ResourceType: result.ResourceType,
Unequipped: result.Unequipped,
UpdatedAtMs: result.UpdatedAtMS,
}, nil
}
func (s *Server) BatchGetUserEquippedResources(ctx context.Context, req *walletv1.BatchGetUserEquippedResourcesRequest) (*walletv1.BatchGetUserEquippedResourcesResponse, error) {
items, err := s.svc.BatchGetUserEquippedResources(ctx, resourcedomain.BatchGetUserEquippedResourcesQuery{
AppCode: req.GetAppCode(),
UserIDs: req.GetUserIds(),
ResourceTypes: req.GetResourceTypes(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &walletv1.BatchGetUserEquippedResourcesResponse{Users: make([]*walletv1.UserEquippedResources, 0, len(items))}
for _, item := range items {
user := &walletv1.UserEquippedResources{
UserId: item.UserID,
Resources: make([]*walletv1.UserResourceEntitlement, 0, len(item.Resources)),
}
for _, resource := range item.Resources {
user.Resources = append(user.Resources, userResourceToProto(resource))
}
resp.Users = append(resp.Users, user)
}
return resp, nil
}
func (s *Server) ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error) {
items, total, err := s.svc.ListResourceGrants(ctx, resourcedomain.ListResourceGrantsQuery{
AppCode: req.GetAppCode(),

View File

@ -46,6 +46,52 @@ func (s *Server) DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest)
return nil, xerr.ToGRPCError(err)
}
return debitGiftResponseFromReceipt(receipt), nil
}
// BatchDebitGift 只负责批量目标 protobuf 转换;同事务扣费和幂等由 wallet service/repository 保证。
func (s *Server) BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
ctx = appcode.WithContext(ctx, req.GetAppCode())
targets := make([]ledger.DebitGiftTargetCommand, 0, len(req.GetTargets()))
for _, target := range req.GetTargets() {
targets = append(targets, ledger.DebitGiftTargetCommand{
CommandID: target.GetCommandId(),
TargetUserID: target.GetTargetUserId(),
TargetIsHost: target.GetTargetIsHost(),
TargetHostRegionID: target.GetTargetHostRegionId(),
TargetAgencyOwnerUserID: target.GetTargetAgencyOwnerUserId(),
})
}
receipt, err := s.svc.BatchDebitGift(ctx, ledger.BatchDebitGiftCommand{
AppCode: req.GetAppCode(),
CommandID: req.GetCommandId(),
RoomID: req.GetRoomId(),
SenderUserID: req.GetSenderUserId(),
GiftID: req.GetGiftId(),
GiftCount: req.GetGiftCount(),
PriceVersion: req.GetPriceVersion(),
RegionID: req.GetRegionId(),
SenderRegionID: req.GetSenderRegionId(),
Targets: targets,
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
response := &walletv1.BatchDebitGiftResponse{
Aggregate: debitGiftResponseFromReceipt(receipt.Aggregate),
Receipts: make([]*walletv1.BatchDebitGiftReceipt, 0, len(receipt.Targets)),
}
for _, target := range receipt.Targets {
response.Receipts = append(response.Receipts, &walletv1.BatchDebitGiftReceipt{
TargetUserId: target.TargetUserID,
CommandId: target.CommandID,
Billing: debitGiftResponseFromReceipt(target.Receipt),
})
}
return response, nil
}
func debitGiftResponseFromReceipt(receipt ledger.Receipt) *walletv1.DebitGiftResponse {
return &walletv1.DebitGiftResponse{
BillingReceiptId: receipt.BillingReceiptID,
BalanceAfter: receipt.BalanceAfter,
@ -59,7 +105,7 @@ func (s *Server) DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest)
PriceVersion: receipt.PriceVersion,
HostPeriodDiamondAdded: receipt.HostPeriodDiamondAdded,
HostPeriodCycleKey: receipt.HostPeriodCycleKey,
}, nil
}
}
// GetBalances 返回用户多资产余额投影。