机器人房
This commit is contained in:
parent
604fcebda4
commit
d455d24016
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// protoc v5.29.2
|
||||
// source: proto/activity/v1/activity.proto
|
||||
|
||||
package activityv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// - protoc v5.29.2
|
||||
// source: proto/activity/v1/activity.proto
|
||||
|
||||
package activityv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// protoc v5.29.2
|
||||
// source: proto/events/room/v1/events.proto
|
||||
|
||||
package roomeventsv1
|
||||
@ -1209,6 +1209,10 @@ type RoomGiftSent struct {
|
||||
GiftAnimationUrl string `protobuf:"bytes,17,opt,name=gift_animation_url,json=giftAnimationUrl,proto3" json:"gift_animation_url,omitempty"`
|
||||
TargetGiftValue int64 `protobuf:"varint,18,opt,name=target_gift_value,json=targetGiftValue,proto3" json:"target_gift_value,omitempty"`
|
||||
GiftEffectTypes []string `protobuf:"bytes,19,rep,name=gift_effect_types,json=giftEffectTypes,proto3" json:"gift_effect_types,omitempty"`
|
||||
// is_robot_gift 标记机器人房间展示用礼物事实;下游真实统计、任务和结算消费者必须跳过。
|
||||
IsRobotGift bool `protobuf:"varint,20,opt,name=is_robot_gift,json=isRobotGift,proto3" json:"is_robot_gift,omitempty"`
|
||||
// synthetic_lucky_gift 标记幸运礼物只用于房间表现,不写真实抽奖流水、不扣真实奖池、不发真实奖励。
|
||||
SyntheticLuckyGift bool `protobuf:"varint,21,opt,name=synthetic_lucky_gift,json=syntheticLuckyGift,proto3" json:"synthetic_lucky_gift,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -1376,6 +1380,186 @@ func (x *RoomGiftSent) GetGiftEffectTypes() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *RoomGiftSent) GetIsRobotGift() bool {
|
||||
if x != nil {
|
||||
return x.IsRobotGift
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *RoomGiftSent) GetSyntheticLuckyGift() bool {
|
||||
if x != nil {
|
||||
return x.SyntheticLuckyGift
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RoomRobotLuckyGiftDrawn 是机器人房间专用幸运礼物展示事件。
|
||||
// 它复用客户端 lucky_gift_drawn 表现协议,但不代表真实奖池抽奖和钱包返奖事实。
|
||||
type RoomRobotLuckyGiftDrawn struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
DrawId string `protobuf:"bytes,1,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"`
|
||||
CommandId string `protobuf:"bytes,2,opt,name=command_id,json=commandId,proto3" json:"command_id,omitempty"`
|
||||
SenderUserId int64 `protobuf:"varint,3,opt,name=sender_user_id,json=senderUserId,proto3" json:"sender_user_id,omitempty"`
|
||||
TargetUserId int64 `protobuf:"varint,4,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||
GiftId string `protobuf:"bytes,5,opt,name=gift_id,json=giftId,proto3" json:"gift_id,omitempty"`
|
||||
GiftCount int32 `protobuf:"varint,6,opt,name=gift_count,json=giftCount,proto3" json:"gift_count,omitempty"`
|
||||
PoolId string `protobuf:"bytes,7,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"`
|
||||
MultiplierPpm int64 `protobuf:"varint,8,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"`
|
||||
BaseRewardCoins int64 `protobuf:"varint,9,opt,name=base_reward_coins,json=baseRewardCoins,proto3" json:"base_reward_coins,omitempty"`
|
||||
RoomAtmosphereRewardCoins int64 `protobuf:"varint,10,opt,name=room_atmosphere_reward_coins,json=roomAtmosphereRewardCoins,proto3" json:"room_atmosphere_reward_coins,omitempty"`
|
||||
ActivitySubsidyCoins int64 `protobuf:"varint,11,opt,name=activity_subsidy_coins,json=activitySubsidyCoins,proto3" json:"activity_subsidy_coins,omitempty"`
|
||||
EffectiveRewardCoins int64 `protobuf:"varint,12,opt,name=effective_reward_coins,json=effectiveRewardCoins,proto3" json:"effective_reward_coins,omitempty"`
|
||||
CreatedAtMs int64 `protobuf:"varint,13,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
|
||||
VisibleRegionId int64 `protobuf:"varint,14,opt,name=visible_region_id,json=visibleRegionId,proto3" json:"visible_region_id,omitempty"`
|
||||
IsRobot bool `protobuf:"varint,15,opt,name=is_robot,json=isRobot,proto3" json:"is_robot,omitempty"`
|
||||
Synthetic bool `protobuf:"varint,16,opt,name=synthetic,proto3" json:"synthetic,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) Reset() {
|
||||
*x = RoomRobotLuckyGiftDrawn{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*RoomRobotLuckyGiftDrawn) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[17]
|
||||
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 RoomRobotLuckyGiftDrawn.ProtoReflect.Descriptor instead.
|
||||
func (*RoomRobotLuckyGiftDrawn) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{17}
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetDrawId() string {
|
||||
if x != nil {
|
||||
return x.DrawId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetCommandId() string {
|
||||
if x != nil {
|
||||
return x.CommandId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetSenderUserId() int64 {
|
||||
if x != nil {
|
||||
return x.SenderUserId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetTargetUserId() int64 {
|
||||
if x != nil {
|
||||
return x.TargetUserId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetGiftId() string {
|
||||
if x != nil {
|
||||
return x.GiftId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetGiftCount() int32 {
|
||||
if x != nil {
|
||||
return x.GiftCount
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetPoolId() string {
|
||||
if x != nil {
|
||||
return x.PoolId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetMultiplierPpm() int64 {
|
||||
if x != nil {
|
||||
return x.MultiplierPpm
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetBaseRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.BaseRewardCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetRoomAtmosphereRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.RoomAtmosphereRewardCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetActivitySubsidyCoins() int64 {
|
||||
if x != nil {
|
||||
return x.ActivitySubsidyCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetEffectiveRewardCoins() int64 {
|
||||
if x != nil {
|
||||
return x.EffectiveRewardCoins
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetCreatedAtMs() int64 {
|
||||
if x != nil {
|
||||
return x.CreatedAtMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetVisibleRegionId() int64 {
|
||||
if x != nil {
|
||||
return x.VisibleRegionId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetIsRobot() bool {
|
||||
if x != nil {
|
||||
return x.IsRobot
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *RoomRobotLuckyGiftDrawn) GetSynthetic() bool {
|
||||
if x != nil {
|
||||
return x.Synthetic
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// RoomHeatChanged 表达热度变化结果。
|
||||
type RoomHeatChanged struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
@ -1387,7 +1571,7 @@ type RoomHeatChanged struct {
|
||||
|
||||
func (x *RoomHeatChanged) Reset() {
|
||||
*x = RoomHeatChanged{}
|
||||
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)
|
||||
}
|
||||
@ -1399,7 +1583,7 @@ func (x *RoomHeatChanged) String() string {
|
||||
func (*RoomHeatChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomHeatChanged) 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 {
|
||||
@ -1412,7 +1596,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{17}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{18}
|
||||
}
|
||||
|
||||
func (x *RoomHeatChanged) GetDelta() int64 {
|
||||
@ -1441,7 +1625,7 @@ type RoomRankChanged struct {
|
||||
|
||||
func (x *RoomRankChanged) Reset() {
|
||||
*x = RoomRankChanged{}
|
||||
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)
|
||||
}
|
||||
@ -1453,7 +1637,7 @@ func (x *RoomRankChanged) String() string {
|
||||
func (*RoomRankChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRankChanged) 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 {
|
||||
@ -1466,7 +1650,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{18}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
|
||||
}
|
||||
|
||||
func (x *RoomRankChanged) GetUserId() int64 {
|
||||
@ -1506,7 +1690,7 @@ type RoomRocketRewardGrant struct {
|
||||
|
||||
func (x *RoomRocketRewardGrant) Reset() {
|
||||
*x = RoomRocketRewardGrant{}
|
||||
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)
|
||||
}
|
||||
@ -1518,7 +1702,7 @@ func (x *RoomRocketRewardGrant) String() string {
|
||||
func (*RoomRocketRewardGrant) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRocketRewardGrant) 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 {
|
||||
@ -1531,7 +1715,7 @@ func (x *RoomRocketRewardGrant) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomRocketRewardGrant.ProtoReflect.Descriptor instead.
|
||||
func (*RoomRocketRewardGrant) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{19}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
|
||||
}
|
||||
|
||||
func (x *RoomRocketRewardGrant) GetRewardRole() string {
|
||||
@ -1613,7 +1797,7 @@ type RoomRocketFuelChanged struct {
|
||||
|
||||
func (x *RoomRocketFuelChanged) Reset() {
|
||||
*x = RoomRocketFuelChanged{}
|
||||
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)
|
||||
}
|
||||
@ -1625,7 +1809,7 @@ func (x *RoomRocketFuelChanged) String() string {
|
||||
func (*RoomRocketFuelChanged) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRocketFuelChanged) 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 {
|
||||
@ -1638,7 +1822,7 @@ func (x *RoomRocketFuelChanged) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomRocketFuelChanged.ProtoReflect.Descriptor instead.
|
||||
func (*RoomRocketFuelChanged) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{20}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
|
||||
}
|
||||
|
||||
func (x *RoomRocketFuelChanged) GetRocketId() string {
|
||||
@ -1762,7 +1946,7 @@ type RoomRocketIgnited struct {
|
||||
|
||||
func (x *RoomRocketIgnited) Reset() {
|
||||
*x = RoomRocketIgnited{}
|
||||
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)
|
||||
}
|
||||
@ -1774,7 +1958,7 @@ func (x *RoomRocketIgnited) String() string {
|
||||
func (*RoomRocketIgnited) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRocketIgnited) 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 {
|
||||
@ -1787,7 +1971,7 @@ func (x *RoomRocketIgnited) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomRocketIgnited.ProtoReflect.Descriptor instead.
|
||||
func (*RoomRocketIgnited) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{21}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22}
|
||||
}
|
||||
|
||||
func (x *RoomRocketIgnited) GetRocketId() string {
|
||||
@ -1906,7 +2090,7 @@ type RoomRocketLaunched struct {
|
||||
|
||||
func (x *RoomRocketLaunched) Reset() {
|
||||
*x = RoomRocketLaunched{}
|
||||
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)
|
||||
}
|
||||
@ -1918,7 +2102,7 @@ func (x *RoomRocketLaunched) String() string {
|
||||
func (*RoomRocketLaunched) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRocketLaunched) 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 {
|
||||
@ -1931,7 +2115,7 @@ func (x *RoomRocketLaunched) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomRocketLaunched.ProtoReflect.Descriptor instead.
|
||||
func (*RoomRocketLaunched) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{22}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{23}
|
||||
}
|
||||
|
||||
func (x *RoomRocketLaunched) GetRocketId() string {
|
||||
@ -2009,7 +2193,7 @@ type RoomRocketRewardGranted struct {
|
||||
|
||||
func (x *RoomRocketRewardGranted) Reset() {
|
||||
*x = RoomRocketRewardGranted{}
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[24]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@ -2021,7 +2205,7 @@ func (x *RoomRocketRewardGranted) String() string {
|
||||
func (*RoomRocketRewardGranted) ProtoMessage() {}
|
||||
|
||||
func (x *RoomRocketRewardGranted) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[23]
|
||||
mi := &file_proto_events_room_v1_events_proto_msgTypes[24]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@ -2034,7 +2218,7 @@ func (x *RoomRocketRewardGranted) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use RoomRocketRewardGranted.ProtoReflect.Descriptor instead.
|
||||
func (*RoomRocketRewardGranted) Descriptor() ([]byte, []int) {
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{23}
|
||||
return file_proto_events_room_v1_events_proto_rawDescGZIP(), []int{24}
|
||||
}
|
||||
|
||||
func (x *RoomRocketRewardGranted) GetRocketId() string {
|
||||
@ -2160,7 +2344,7 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\\\n" +
|
||||
"\x10RoomUserUnbanned\x12\"\n" +
|
||||
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
|
||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\xb5\x05\n" +
|
||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\x8b\x06\n" +
|
||||
"\fRoomGiftSent\x12$\n" +
|
||||
"\x0esender_user_id\x18\x01 \x01(\x03R\fsenderUserId\x12$\n" +
|
||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x17\n" +
|
||||
@ -2186,7 +2370,29 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
||||
"\rgift_icon_url\x18\x10 \x01(\tR\vgiftIconUrl\x12,\n" +
|
||||
"\x12gift_animation_url\x18\x11 \x01(\tR\x10giftAnimationUrl\x12*\n" +
|
||||
"\x11target_gift_value\x18\x12 \x01(\x03R\x0ftargetGiftValue\x12*\n" +
|
||||
"\x11gift_effect_types\x18\x13 \x03(\tR\x0fgiftEffectTypes\"J\n" +
|
||||
"\x11gift_effect_types\x18\x13 \x03(\tR\x0fgiftEffectTypes\x12\"\n" +
|
||||
"\ris_robot_gift\x18\x14 \x01(\bR\visRobotGift\x120\n" +
|
||||
"\x14synthetic_lucky_gift\x18\x15 \x01(\bR\x12syntheticLuckyGift\"\xf7\x04\n" +
|
||||
"\x17RoomRobotLuckyGiftDrawn\x12\x17\n" +
|
||||
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"command_id\x18\x02 \x01(\tR\tcommandId\x12$\n" +
|
||||
"\x0esender_user_id\x18\x03 \x01(\x03R\fsenderUserId\x12$\n" +
|
||||
"\x0etarget_user_id\x18\x04 \x01(\x03R\ftargetUserId\x12\x17\n" +
|
||||
"\agift_id\x18\x05 \x01(\tR\x06giftId\x12\x1d\n" +
|
||||
"\n" +
|
||||
"gift_count\x18\x06 \x01(\x05R\tgiftCount\x12\x17\n" +
|
||||
"\apool_id\x18\a \x01(\tR\x06poolId\x12%\n" +
|
||||
"\x0emultiplier_ppm\x18\b \x01(\x03R\rmultiplierPpm\x12*\n" +
|
||||
"\x11base_reward_coins\x18\t \x01(\x03R\x0fbaseRewardCoins\x12?\n" +
|
||||
"\x1croom_atmosphere_reward_coins\x18\n" +
|
||||
" \x01(\x03R\x19roomAtmosphereRewardCoins\x124\n" +
|
||||
"\x16activity_subsidy_coins\x18\v \x01(\x03R\x14activitySubsidyCoins\x124\n" +
|
||||
"\x16effective_reward_coins\x18\f \x01(\x03R\x14effectiveRewardCoins\x12\"\n" +
|
||||
"\rcreated_at_ms\x18\r \x01(\x03R\vcreatedAtMs\x12*\n" +
|
||||
"\x11visible_region_id\x18\x0e \x01(\x03R\x0fvisibleRegionId\x12\x19\n" +
|
||||
"\bis_robot\x18\x0f \x01(\bR\aisRobot\x12\x1c\n" +
|
||||
"\tsynthetic\x18\x10 \x01(\bR\tsynthetic\"J\n" +
|
||||
"\x0fRoomHeatChanged\x12\x14\n" +
|
||||
"\x05delta\x18\x01 \x01(\x03R\x05delta\x12!\n" +
|
||||
"\fcurrent_heat\x18\x02 \x01(\x03R\vcurrentHeat\"_\n" +
|
||||
@ -2272,7 +2478,7 @@ 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, 24)
|
||||
var file_proto_events_room_v1_events_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
|
||||
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
|
||||
@ -2291,18 +2497,19 @@ var file_proto_events_room_v1_events_proto_goTypes = []any{
|
||||
(*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
|
||||
(*RoomRocketRewardGrant)(nil), // 19: hyapp.events.room.v1.RoomRocketRewardGrant
|
||||
(*RoomRocketFuelChanged)(nil), // 20: hyapp.events.room.v1.RoomRocketFuelChanged
|
||||
(*RoomRocketIgnited)(nil), // 21: hyapp.events.room.v1.RoomRocketIgnited
|
||||
(*RoomRocketLaunched)(nil), // 22: hyapp.events.room.v1.RoomRocketLaunched
|
||||
(*RoomRocketRewardGranted)(nil), // 23: hyapp.events.room.v1.RoomRocketRewardGranted
|
||||
(*RoomRobotLuckyGiftDrawn)(nil), // 17: hyapp.events.room.v1.RoomRobotLuckyGiftDrawn
|
||||
(*RoomHeatChanged)(nil), // 18: hyapp.events.room.v1.RoomHeatChanged
|
||||
(*RoomRankChanged)(nil), // 19: hyapp.events.room.v1.RoomRankChanged
|
||||
(*RoomRocketRewardGrant)(nil), // 20: hyapp.events.room.v1.RoomRocketRewardGrant
|
||||
(*RoomRocketFuelChanged)(nil), // 21: hyapp.events.room.v1.RoomRocketFuelChanged
|
||||
(*RoomRocketIgnited)(nil), // 22: hyapp.events.room.v1.RoomRocketIgnited
|
||||
(*RoomRocketLaunched)(nil), // 23: hyapp.events.room.v1.RoomRocketLaunched
|
||||
(*RoomRocketRewardGranted)(nil), // 24: hyapp.events.room.v1.RoomRocketRewardGranted
|
||||
}
|
||||
var file_proto_events_room_v1_events_proto_depIdxs = []int32{
|
||||
4, // 0: hyapp.events.room.v1.RoomUserJoined.entry_vehicle:type_name -> hyapp.events.room.v1.RoomEntryVehicleSnapshot
|
||||
19, // 1: hyapp.events.room.v1.RoomRocketLaunched.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant
|
||||
19, // 2: hyapp.events.room.v1.RoomRocketRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant
|
||||
20, // 1: hyapp.events.room.v1.RoomRocketLaunched.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant
|
||||
20, // 2: hyapp.events.room.v1.RoomRocketRewardGranted.rewards:type_name -> hyapp.events.room.v1.RoomRocketRewardGrant
|
||||
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
|
||||
@ -2321,7 +2528,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: 24,
|
||||
NumMessages: 25,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@ -166,6 +166,31 @@ message RoomGiftSent {
|
||||
string gift_animation_url = 17;
|
||||
int64 target_gift_value = 18;
|
||||
repeated string gift_effect_types = 19;
|
||||
// is_robot_gift 标记机器人房间展示用礼物事实;下游真实统计、任务和结算消费者必须跳过。
|
||||
bool is_robot_gift = 20;
|
||||
// synthetic_lucky_gift 标记幸运礼物只用于房间表现,不写真实抽奖流水、不扣真实奖池、不发真实奖励。
|
||||
bool synthetic_lucky_gift = 21;
|
||||
}
|
||||
|
||||
// RoomRobotLuckyGiftDrawn 是机器人房间专用幸运礼物展示事件。
|
||||
// 它复用客户端 lucky_gift_drawn 表现协议,但不代表真实奖池抽奖和钱包返奖事实。
|
||||
message RoomRobotLuckyGiftDrawn {
|
||||
string draw_id = 1;
|
||||
string command_id = 2;
|
||||
int64 sender_user_id = 3;
|
||||
int64 target_user_id = 4;
|
||||
string gift_id = 5;
|
||||
int32 gift_count = 6;
|
||||
string pool_id = 7;
|
||||
int64 multiplier_ppm = 8;
|
||||
int64 base_reward_coins = 9;
|
||||
int64 room_atmosphere_reward_coins = 10;
|
||||
int64 activity_subsidy_coins = 11;
|
||||
int64 effective_reward_coins = 12;
|
||||
int64 created_at_ms = 13;
|
||||
int64 visible_region_id = 14;
|
||||
bool is_robot = 15;
|
||||
bool synthetic = 16;
|
||||
}
|
||||
|
||||
// RoomHeatChanged 表达热度变化结果。
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// protoc v5.29.2
|
||||
// source: proto/game/v1/game.proto
|
||||
|
||||
package gamev1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// - protoc v5.29.2
|
||||
// source: proto/game/v1/game.proto
|
||||
|
||||
package gamev1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// protoc v5.29.2
|
||||
// source: proto/robot/v1/robot.proto
|
||||
|
||||
package robotv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// - protoc v5.29.2
|
||||
// source: proto/robot/v1/robot.proto
|
||||
|
||||
package robotv1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -338,6 +338,85 @@ message AdminCancelRoomPinResponse {
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
message AdminRobotRoomGiftRule {
|
||||
repeated string gift_ids = 1;
|
||||
repeated string lucky_gift_ids = 2;
|
||||
int64 normal_gift_interval_ms = 3;
|
||||
int64 lucky_combo_min = 4;
|
||||
int64 lucky_combo_max = 5;
|
||||
int64 lucky_pause_min_ms = 6;
|
||||
int64 lucky_pause_max_ms = 7;
|
||||
}
|
||||
|
||||
message AdminRobotRoom {
|
||||
string app_code = 1;
|
||||
string room_id = 2;
|
||||
string room_short_id = 3;
|
||||
string title = 4;
|
||||
string cover_url = 5;
|
||||
int64 visible_region_id = 6;
|
||||
string status = 7;
|
||||
int64 owner_robot_user_id = 8;
|
||||
repeated int64 robot_user_ids = 9;
|
||||
AdminRobotRoomGiftRule gift_rule = 10;
|
||||
uint64 created_by_admin_id = 11;
|
||||
int64 created_at_ms = 12;
|
||||
int64 updated_at_ms = 13;
|
||||
}
|
||||
|
||||
message AdminListRobotRoomsRequest {
|
||||
RequestMeta meta = 1;
|
||||
int32 page = 2;
|
||||
int32 page_size = 3;
|
||||
string status = 4;
|
||||
}
|
||||
|
||||
message AdminListRobotRoomsResponse {
|
||||
repeated AdminRobotRoom rooms = 1;
|
||||
int64 total = 2;
|
||||
int64 server_time_ms = 3;
|
||||
}
|
||||
|
||||
message AdminCreateRobotRoomRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 owner_robot_user_id = 2;
|
||||
repeated int64 candidate_robot_user_ids = 3;
|
||||
int32 min_robot_count = 4;
|
||||
int32 max_robot_count = 5;
|
||||
string room_name = 6;
|
||||
string room_avatar = 7;
|
||||
int64 visible_region_id = 8;
|
||||
AdminRobotRoomGiftRule gift_rule = 9;
|
||||
uint64 admin_id = 10;
|
||||
}
|
||||
|
||||
message AdminCreateRobotRoomResponse {
|
||||
AdminRobotRoom room = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
message AdminSetRobotRoomStatusRequest {
|
||||
RequestMeta meta = 1;
|
||||
string status = 2;
|
||||
uint64 admin_id = 3;
|
||||
}
|
||||
|
||||
message AdminSetRobotRoomStatusResponse {
|
||||
AdminRobotRoom room = 1;
|
||||
int64 server_time_ms = 2;
|
||||
}
|
||||
|
||||
message AdminFilterAvailableRoomRobotsRequest {
|
||||
RequestMeta meta = 1;
|
||||
repeated int64 user_ids = 2;
|
||||
}
|
||||
|
||||
message AdminFilterAvailableRoomRobotsResponse {
|
||||
repeated int64 available_user_ids = 1;
|
||||
repeated int64 occupied_user_ids = 2;
|
||||
int64 server_time_ms = 3;
|
||||
}
|
||||
|
||||
// RoomBanState 表达单个房间 ban 的治理状态。
|
||||
message RoomBanState {
|
||||
int64 user_id = 1;
|
||||
@ -440,6 +519,10 @@ message CreateRoomRequest {
|
||||
string room_short_id = 8;
|
||||
// owner_country_code 由 gateway 按创建者 user-service 当前国家填充,用于发现页国家分类筛选。
|
||||
string owner_country_code = 9;
|
||||
// robot_room 只供内部机器人房间编排器设置;gateway 不接受客户端提交该字段。
|
||||
bool robot_room = 10;
|
||||
// robot_user_ids 是该机器人房间允许参与机器人送礼和虚拟上麦的用户集合。
|
||||
repeated int64 robot_user_ids = 11;
|
||||
}
|
||||
|
||||
// CreateRoomResponse 返回新建后的房间快照。
|
||||
@ -1149,6 +1232,8 @@ service RoomCommandService {
|
||||
rpc AdminUpdateRoomSeatConfig(AdminUpdateRoomSeatConfigRequest) returns (AdminUpdateRoomSeatConfigResponse);
|
||||
rpc AdminCreateRoomPin(AdminCreateRoomPinRequest) returns (AdminCreateRoomPinResponse);
|
||||
rpc AdminCancelRoomPin(AdminCancelRoomPinRequest) returns (AdminCancelRoomPinResponse);
|
||||
rpc AdminCreateRobotRoom(AdminCreateRobotRoomRequest) returns (AdminCreateRobotRoomResponse);
|
||||
rpc AdminSetRobotRoomStatus(AdminSetRobotRoomStatusRequest) returns (AdminSetRobotRoomStatusResponse);
|
||||
rpc MicUp(MicUpRequest) returns (MicUpResponse);
|
||||
rpc MicDown(MicDownRequest) returns (MicDownResponse);
|
||||
rpc ChangeMicSeat(ChangeMicSeatRequest) returns (ChangeMicSeatResponse);
|
||||
@ -1182,6 +1267,8 @@ service RoomQueryService {
|
||||
rpc AdminGetRoomRocketConfig(AdminGetRoomRocketConfigRequest) returns (AdminGetRoomRocketConfigResponse);
|
||||
rpc AdminGetRoomSeatConfig(AdminGetRoomSeatConfigRequest) returns (AdminGetRoomSeatConfigResponse);
|
||||
rpc AdminListRoomPins(AdminListRoomPinsRequest) returns (AdminListRoomPinsResponse);
|
||||
rpc AdminListRobotRooms(AdminListRobotRoomsRequest) returns (AdminListRobotRoomsResponse);
|
||||
rpc AdminFilterAvailableRoomRobots(AdminFilterAvailableRoomRobotsRequest) returns (AdminFilterAvailableRoomRobotsResponse);
|
||||
rpc ListRooms(ListRoomsRequest) returns (ListRoomsResponse);
|
||||
rpc ListRoomFeeds(ListRoomFeedsRequest) returns (ListRoomsResponse);
|
||||
rpc ListRoomGiftLeaderboard(ListRoomGiftLeaderboardRequest) returns (ListRoomGiftLeaderboardResponse);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// - protoc v5.29.2
|
||||
// source: proto/room/v1/room.proto
|
||||
|
||||
package roomv1
|
||||
@ -33,6 +33,8 @@ const (
|
||||
RoomCommandService_AdminUpdateRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminUpdateRoomSeatConfig"
|
||||
RoomCommandService_AdminCreateRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCreateRoomPin"
|
||||
RoomCommandService_AdminCancelRoomPin_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCancelRoomPin"
|
||||
RoomCommandService_AdminCreateRobotRoom_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminCreateRobotRoom"
|
||||
RoomCommandService_AdminSetRobotRoomStatus_FullMethodName = "/hyapp.room.v1.RoomCommandService/AdminSetRobotRoomStatus"
|
||||
RoomCommandService_MicUp_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicUp"
|
||||
RoomCommandService_MicDown_FullMethodName = "/hyapp.room.v1.RoomCommandService/MicDown"
|
||||
RoomCommandService_ChangeMicSeat_FullMethodName = "/hyapp.room.v1.RoomCommandService/ChangeMicSeat"
|
||||
@ -73,6 +75,8 @@ type RoomCommandServiceClient interface {
|
||||
AdminUpdateRoomSeatConfig(ctx context.Context, in *AdminUpdateRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminUpdateRoomSeatConfigResponse, error)
|
||||
AdminCreateRoomPin(ctx context.Context, in *AdminCreateRoomPinRequest, opts ...grpc.CallOption) (*AdminCreateRoomPinResponse, error)
|
||||
AdminCancelRoomPin(ctx context.Context, in *AdminCancelRoomPinRequest, opts ...grpc.CallOption) (*AdminCancelRoomPinResponse, error)
|
||||
AdminCreateRobotRoom(ctx context.Context, in *AdminCreateRobotRoomRequest, opts ...grpc.CallOption) (*AdminCreateRobotRoomResponse, error)
|
||||
AdminSetRobotRoomStatus(ctx context.Context, in *AdminSetRobotRoomStatusRequest, opts ...grpc.CallOption) (*AdminSetRobotRoomStatusResponse, error)
|
||||
MicUp(ctx context.Context, in *MicUpRequest, opts ...grpc.CallOption) (*MicUpResponse, error)
|
||||
MicDown(ctx context.Context, in *MicDownRequest, opts ...grpc.CallOption) (*MicDownResponse, error)
|
||||
ChangeMicSeat(ctx context.Context, in *ChangeMicSeatRequest, opts ...grpc.CallOption) (*ChangeMicSeatResponse, error)
|
||||
@ -241,6 +245,26 @@ func (c *roomCommandServiceClient) AdminCancelRoomPin(ctx context.Context, in *A
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomCommandServiceClient) AdminCreateRobotRoom(ctx context.Context, in *AdminCreateRobotRoomRequest, opts ...grpc.CallOption) (*AdminCreateRobotRoomResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AdminCreateRobotRoomResponse)
|
||||
err := c.cc.Invoke(ctx, RoomCommandService_AdminCreateRobotRoom_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomCommandServiceClient) AdminSetRobotRoomStatus(ctx context.Context, in *AdminSetRobotRoomStatusRequest, opts ...grpc.CallOption) (*AdminSetRobotRoomStatusResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AdminSetRobotRoomStatusResponse)
|
||||
err := c.cc.Invoke(ctx, RoomCommandService_AdminSetRobotRoomStatus_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomCommandServiceClient) MicUp(ctx context.Context, in *MicUpRequest, opts ...grpc.CallOption) (*MicUpResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(MicUpResponse)
|
||||
@ -441,6 +465,8 @@ type RoomCommandServiceServer interface {
|
||||
AdminUpdateRoomSeatConfig(context.Context, *AdminUpdateRoomSeatConfigRequest) (*AdminUpdateRoomSeatConfigResponse, error)
|
||||
AdminCreateRoomPin(context.Context, *AdminCreateRoomPinRequest) (*AdminCreateRoomPinResponse, error)
|
||||
AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error)
|
||||
AdminCreateRobotRoom(context.Context, *AdminCreateRobotRoomRequest) (*AdminCreateRobotRoomResponse, error)
|
||||
AdminSetRobotRoomStatus(context.Context, *AdminSetRobotRoomStatusRequest) (*AdminSetRobotRoomStatusResponse, error)
|
||||
MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error)
|
||||
MicDown(context.Context, *MicDownRequest) (*MicDownResponse, error)
|
||||
ChangeMicSeat(context.Context, *ChangeMicSeatRequest) (*ChangeMicSeatResponse, error)
|
||||
@ -511,6 +537,12 @@ func (UnimplementedRoomCommandServiceServer) AdminCreateRoomPin(context.Context,
|
||||
func (UnimplementedRoomCommandServiceServer) AdminCancelRoomPin(context.Context, *AdminCancelRoomPinRequest) (*AdminCancelRoomPinResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCancelRoomPin not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminCreateRobotRoom(context.Context, *AdminCreateRobotRoomRequest) (*AdminCreateRobotRoomResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminCreateRobotRoom not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) AdminSetRobotRoomStatus(context.Context, *AdminSetRobotRoomStatusRequest) (*AdminSetRobotRoomStatusResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminSetRobotRoomStatus not implemented")
|
||||
}
|
||||
func (UnimplementedRoomCommandServiceServer) MicUp(context.Context, *MicUpRequest) (*MicUpResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method MicUp not implemented")
|
||||
}
|
||||
@ -838,6 +870,42 @@ func _RoomCommandService_AdminCancelRoomPin_Handler(srv interface{}, ctx context
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomCommandService_AdminCreateRobotRoom_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminCreateRobotRoomRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RoomCommandServiceServer).AdminCreateRobotRoom(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RoomCommandService_AdminCreateRobotRoom_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RoomCommandServiceServer).AdminCreateRobotRoom(ctx, req.(*AdminCreateRobotRoomRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomCommandService_AdminSetRobotRoomStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminSetRobotRoomStatusRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RoomCommandServiceServer).AdminSetRobotRoomStatus(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RoomCommandService_AdminSetRobotRoomStatus_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RoomCommandServiceServer).AdminSetRobotRoomStatus(ctx, req.(*AdminSetRobotRoomStatusRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomCommandService_MicUp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(MicUpRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -1225,6 +1293,14 @@ var RoomCommandService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "AdminCancelRoomPin",
|
||||
Handler: _RoomCommandService_AdminCancelRoomPin_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminCreateRobotRoom",
|
||||
Handler: _RoomCommandService_AdminCreateRobotRoom_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminSetRobotRoomStatus",
|
||||
Handler: _RoomCommandService_AdminSetRobotRoomStatus_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "MicUp",
|
||||
Handler: _RoomCommandService_MicUp_Handler,
|
||||
@ -1452,6 +1528,8 @@ const (
|
||||
RoomQueryService_AdminGetRoomRocketConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomRocketConfig"
|
||||
RoomQueryService_AdminGetRoomSeatConfig_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminGetRoomSeatConfig"
|
||||
RoomQueryService_AdminListRoomPins_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRoomPins"
|
||||
RoomQueryService_AdminListRobotRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminListRobotRooms"
|
||||
RoomQueryService_AdminFilterAvailableRoomRobots_FullMethodName = "/hyapp.room.v1.RoomQueryService/AdminFilterAvailableRoomRobots"
|
||||
RoomQueryService_ListRooms_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRooms"
|
||||
RoomQueryService_ListRoomFeeds_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomFeeds"
|
||||
RoomQueryService_ListRoomGiftLeaderboard_FullMethodName = "/hyapp.room.v1.RoomQueryService/ListRoomGiftLeaderboard"
|
||||
@ -1475,6 +1553,8 @@ type RoomQueryServiceClient interface {
|
||||
AdminGetRoomRocketConfig(ctx context.Context, in *AdminGetRoomRocketConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomRocketConfigResponse, error)
|
||||
AdminGetRoomSeatConfig(ctx context.Context, in *AdminGetRoomSeatConfigRequest, opts ...grpc.CallOption) (*AdminGetRoomSeatConfigResponse, error)
|
||||
AdminListRoomPins(ctx context.Context, in *AdminListRoomPinsRequest, opts ...grpc.CallOption) (*AdminListRoomPinsResponse, error)
|
||||
AdminListRobotRooms(ctx context.Context, in *AdminListRobotRoomsRequest, opts ...grpc.CallOption) (*AdminListRobotRoomsResponse, error)
|
||||
AdminFilterAvailableRoomRobots(ctx context.Context, in *AdminFilterAvailableRoomRobotsRequest, opts ...grpc.CallOption) (*AdminFilterAvailableRoomRobotsResponse, error)
|
||||
ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
|
||||
ListRoomFeeds(ctx context.Context, in *ListRoomFeedsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error)
|
||||
ListRoomGiftLeaderboard(ctx context.Context, in *ListRoomGiftLeaderboardRequest, opts ...grpc.CallOption) (*ListRoomGiftLeaderboardResponse, error)
|
||||
@ -1545,6 +1625,26 @@ func (c *roomQueryServiceClient) AdminListRoomPins(ctx context.Context, in *Admi
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomQueryServiceClient) AdminListRobotRooms(ctx context.Context, in *AdminListRobotRoomsRequest, opts ...grpc.CallOption) (*AdminListRobotRoomsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AdminListRobotRoomsResponse)
|
||||
err := c.cc.Invoke(ctx, RoomQueryService_AdminListRobotRooms_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomQueryServiceClient) AdminFilterAvailableRoomRobots(ctx context.Context, in *AdminFilterAvailableRoomRobotsRequest, opts ...grpc.CallOption) (*AdminFilterAvailableRoomRobotsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(AdminFilterAvailableRoomRobotsResponse)
|
||||
err := c.cc.Invoke(ctx, RoomQueryService_AdminFilterAvailableRoomRobots_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *roomQueryServiceClient) ListRooms(ctx context.Context, in *ListRoomsRequest, opts ...grpc.CallOption) (*ListRoomsResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListRoomsResponse)
|
||||
@ -1656,6 +1756,8 @@ type RoomQueryServiceServer interface {
|
||||
AdminGetRoomRocketConfig(context.Context, *AdminGetRoomRocketConfigRequest) (*AdminGetRoomRocketConfigResponse, error)
|
||||
AdminGetRoomSeatConfig(context.Context, *AdminGetRoomSeatConfigRequest) (*AdminGetRoomSeatConfigResponse, error)
|
||||
AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error)
|
||||
AdminListRobotRooms(context.Context, *AdminListRobotRoomsRequest) (*AdminListRobotRoomsResponse, error)
|
||||
AdminFilterAvailableRoomRobots(context.Context, *AdminFilterAvailableRoomRobotsRequest) (*AdminFilterAvailableRoomRobotsResponse, error)
|
||||
ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error)
|
||||
ListRoomFeeds(context.Context, *ListRoomFeedsRequest) (*ListRoomsResponse, error)
|
||||
ListRoomGiftLeaderboard(context.Context, *ListRoomGiftLeaderboardRequest) (*ListRoomGiftLeaderboardResponse, error)
|
||||
@ -1691,6 +1793,12 @@ func (UnimplementedRoomQueryServiceServer) AdminGetRoomSeatConfig(context.Contex
|
||||
func (UnimplementedRoomQueryServiceServer) AdminListRoomPins(context.Context, *AdminListRoomPinsRequest) (*AdminListRoomPinsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminListRoomPins not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminListRobotRooms(context.Context, *AdminListRobotRoomsRequest) (*AdminListRobotRoomsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminListRobotRooms not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) AdminFilterAvailableRoomRobots(context.Context, *AdminFilterAvailableRoomRobotsRequest) (*AdminFilterAvailableRoomRobotsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method AdminFilterAvailableRoomRobots not implemented")
|
||||
}
|
||||
func (UnimplementedRoomQueryServiceServer) ListRooms(context.Context, *ListRoomsRequest) (*ListRoomsResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListRooms not implemented")
|
||||
}
|
||||
@ -1832,6 +1940,42 @@ func _RoomQueryService_AdminListRoomPins_Handler(srv interface{}, ctx context.Co
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomQueryService_AdminListRobotRooms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminListRobotRoomsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RoomQueryServiceServer).AdminListRobotRooms(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RoomQueryService_AdminListRobotRooms_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RoomQueryServiceServer).AdminListRobotRooms(ctx, req.(*AdminListRobotRoomsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomQueryService_AdminFilterAvailableRoomRobots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(AdminFilterAvailableRoomRobotsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(RoomQueryServiceServer).AdminFilterAvailableRoomRobots(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: RoomQueryService_AdminFilterAvailableRoomRobots_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(RoomQueryServiceServer).AdminFilterAvailableRoomRobots(ctx, req.(*AdminFilterAvailableRoomRobotsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _RoomQueryService_ListRooms_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListRoomsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -2039,6 +2183,14 @@ var RoomQueryService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "AdminListRoomPins",
|
||||
Handler: _RoomQueryService_AdminListRoomPins_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminListRobotRooms",
|
||||
Handler: _RoomQueryService_AdminListRobotRooms_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "AdminFilterAvailableRoomRobots",
|
||||
Handler: _RoomQueryService_AdminFilterAvailableRoomRobots_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListRooms",
|
||||
Handler: _RoomQueryService_ListRooms_Handler,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// protoc v5.29.2
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// - protoc v5.29.2
|
||||
// source: proto/user/v1/auth.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// protoc v5.29.2
|
||||
// source: proto/user/v1/host.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// - protoc v5.29.2
|
||||
// source: proto/user/v1/host.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.0
|
||||
// protoc v5.29.2
|
||||
// source: proto/user/v1/user.proto
|
||||
|
||||
package userv1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// - protoc v5.29.2
|
||||
// source: proto/user/v1/user.proto
|
||||
|
||||
package userv1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -91,6 +91,21 @@ message BatchDebitGiftResponse {
|
||||
repeated BatchDebitGiftReceipt receipts = 2;
|
||||
}
|
||||
|
||||
// DebitRobotGiftRequest 是机器人房间内部送礼扣费命令。
|
||||
// 它只扣 ROBOT_COIN,不产生主播钻石、真实充值消费流水或普通礼物墙投影。
|
||||
message DebitRobotGiftRequest {
|
||||
string command_id = 1;
|
||||
string room_id = 2;
|
||||
int64 sender_user_id = 3;
|
||||
int64 target_user_id = 4;
|
||||
string gift_id = 5;
|
||||
int32 gift_count = 6;
|
||||
string price_version = 7;
|
||||
string app_code = 8;
|
||||
int64 region_id = 9;
|
||||
int64 sender_region_id = 10;
|
||||
}
|
||||
|
||||
// AssetBalance 是用户某类资产的余额投影。
|
||||
message AssetBalance {
|
||||
string asset_type = 1;
|
||||
@ -1829,6 +1844,7 @@ service WalletCronService {
|
||||
service WalletService {
|
||||
rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse);
|
||||
rpc BatchDebitGift(BatchDebitGiftRequest) returns (BatchDebitGiftResponse);
|
||||
rpc DebitRobotGift(DebitRobotGiftRequest) returns (DebitGiftResponse);
|
||||
rpc GetBalances(GetBalancesRequest) returns (GetBalancesResponse);
|
||||
rpc GetActiveHostSalaryPolicy(GetActiveHostSalaryPolicyRequest) returns (GetActiveHostSalaryPolicyResponse);
|
||||
rpc GetHostSalaryProgress(GetHostSalaryProgressRequest) returns (GetHostSalaryProgressResponse);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.6.2
|
||||
// - protoc v7.35.0
|
||||
// - protoc v5.29.2
|
||||
// source: proto/wallet/v1/wallet.proto
|
||||
|
||||
package walletv1
|
||||
@ -203,6 +203,7 @@ var WalletCronService_ServiceDesc = grpc.ServiceDesc{
|
||||
const (
|
||||
WalletService_DebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitGift"
|
||||
WalletService_BatchDebitGift_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchDebitGift"
|
||||
WalletService_DebitRobotGift_FullMethodName = "/hyapp.wallet.v1.WalletService/DebitRobotGift"
|
||||
WalletService_GetBalances_FullMethodName = "/hyapp.wallet.v1.WalletService/GetBalances"
|
||||
WalletService_GetActiveHostSalaryPolicy_FullMethodName = "/hyapp.wallet.v1.WalletService/GetActiveHostSalaryPolicy"
|
||||
WalletService_GetHostSalaryProgress_FullMethodName = "/hyapp.wallet.v1.WalletService/GetHostSalaryProgress"
|
||||
@ -290,6 +291,7 @@ const (
|
||||
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)
|
||||
DebitRobotGift(ctx context.Context, in *DebitRobotGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error)
|
||||
GetBalances(ctx context.Context, in *GetBalancesRequest, opts ...grpc.CallOption) (*GetBalancesResponse, error)
|
||||
GetActiveHostSalaryPolicy(ctx context.Context, in *GetActiveHostSalaryPolicyRequest, opts ...grpc.CallOption) (*GetActiveHostSalaryPolicyResponse, error)
|
||||
GetHostSalaryProgress(ctx context.Context, in *GetHostSalaryProgressRequest, opts ...grpc.CallOption) (*GetHostSalaryProgressResponse, error)
|
||||
@ -397,6 +399,16 @@ func (c *walletServiceClient) BatchDebitGift(ctx context.Context, in *BatchDebit
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *walletServiceClient) DebitRobotGift(ctx context.Context, in *DebitRobotGiftRequest, opts ...grpc.CallOption) (*DebitGiftResponse, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DebitGiftResponse)
|
||||
err := c.cc.Invoke(ctx, WalletService_DebitRobotGift_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)
|
||||
@ -1175,6 +1187,7 @@ func (c *walletServiceClient) RetryRedPacketRefund(ctx context.Context, in *Retr
|
||||
type WalletServiceServer interface {
|
||||
DebitGift(context.Context, *DebitGiftRequest) (*DebitGiftResponse, error)
|
||||
BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error)
|
||||
DebitRobotGift(context.Context, *DebitRobotGiftRequest) (*DebitGiftResponse, error)
|
||||
GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error)
|
||||
GetActiveHostSalaryPolicy(context.Context, *GetActiveHostSalaryPolicyRequest) (*GetActiveHostSalaryPolicyResponse, error)
|
||||
GetHostSalaryProgress(context.Context, *GetHostSalaryProgressRequest) (*GetHostSalaryProgressResponse, error)
|
||||
@ -1268,6 +1281,9 @@ func (UnimplementedWalletServiceServer) DebitGift(context.Context, *DebitGiftReq
|
||||
func (UnimplementedWalletServiceServer) BatchDebitGift(context.Context, *BatchDebitGiftRequest) (*BatchDebitGiftResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method BatchDebitGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) DebitRobotGift(context.Context, *DebitRobotGiftRequest) (*DebitGiftResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method DebitRobotGift not implemented")
|
||||
}
|
||||
func (UnimplementedWalletServiceServer) GetBalances(context.Context, *GetBalancesRequest) (*GetBalancesResponse, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method GetBalances not implemented")
|
||||
}
|
||||
@ -1556,6 +1572,24 @@ func _WalletService_BatchDebitGift_Handler(srv interface{}, ctx context.Context,
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _WalletService_DebitRobotGift_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DebitRobotGiftRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(WalletServiceServer).DebitRobotGift(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: WalletService_DebitRobotGift_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(WalletServiceServer).DebitRobotGift(ctx, req.(*DebitRobotGiftRequest))
|
||||
}
|
||||
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 {
|
||||
@ -2957,6 +2991,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "BatchDebitGift",
|
||||
Handler: _WalletService_BatchDebitGift_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DebitRobotGift",
|
||||
Handler: _WalletService_DebitRobotGift_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetBalances",
|
||||
Handler: _WalletService_GetBalances_Handler,
|
||||
|
||||
98
docs/机器人房间产品文档.md
Normal file
98
docs/机器人房间产品文档.md
Normal file
@ -0,0 +1,98 @@
|
||||
# 机器人房间产品文档
|
||||
|
||||
## 背景
|
||||
|
||||
语音房冷启动阶段,房间列表和新房首屏容易显得冷清。机器人房间用于在内部可控范围内营造房间活跃氛围:机器人账号创建房间、进入房间、静音上麦,并在房间内相互送礼,触发客户端已有的礼物动效和幸运礼物展示。
|
||||
|
||||
机器人房间不面向真人主播发放收益,不参与真实抽奖、真实奖池、榜单和结算。
|
||||
|
||||
## 产品目标
|
||||
|
||||
- 后台可创建和控制机器人房间。
|
||||
- 每个机器人房间自动拉取一批未被占用的机器人账号进房。
|
||||
- 机器人可静音上麦,让房间麦位呈现活跃状态。
|
||||
- 机器人之间自动送普通礼物和幸运礼物。
|
||||
- 幸运礼物展示复用客户端现有飘屏/房间展示协议,但不影响真实幸运礼物奖池和流水。
|
||||
- 机器人房间积分和麦位热度正常增长,用于房间内氛围展示。
|
||||
- 机器人礼物不产生真人收益,不污染充值、消费、结算和排行榜数据。
|
||||
|
||||
## 使用入口
|
||||
|
||||
后台管理系统路径:
|
||||
|
||||
`房间管理 / 机器人房间`
|
||||
|
||||
权限:
|
||||
|
||||
- `room-robot:view`:查看机器人房间。
|
||||
- `room-robot:create`:创建机器人房间。
|
||||
- `room-robot:update`:启动/停止机器人房间。
|
||||
|
||||
## 创建机器人房间
|
||||
|
||||
点击 `增加机器人房间` 后配置:
|
||||
|
||||
- 房间名称:机器人房间对外展示名称。
|
||||
- 房主机器人:从未被机器人房间占用的房间机器人账号中选择。
|
||||
- 候选机器人:从未被机器人房间占用的机器人账号中选择,默认可选择全部可用机器人。
|
||||
- 进房人数范围:例如 `6-10`,系统会从候选机器人中随机抽取一个人数。
|
||||
- 礼物合集:普通礼物池,机器人普通送礼时随机选择。
|
||||
- 幸运礼物合集:幸运礼物展示池,机器人幸运送礼时随机选择。
|
||||
- 普通礼物频次:每个机器人每隔固定毫秒数送一次普通礼物。
|
||||
- 幸运礼物连击范围:每轮幸运礼物随机生成连击次数。
|
||||
- 幸运礼物间隔范围:一轮幸运礼物结束后,随机等待一段时间再开始下一轮。
|
||||
|
||||
## 送礼规则
|
||||
|
||||
### 普通礼物
|
||||
|
||||
- 每个机器人独立执行。
|
||||
- 到达配置间隔后,从普通礼物合集中随机选择 1 个礼物。
|
||||
- 随机选择同房间另一个机器人作为收礼人。
|
||||
- 每次只送 1 个目标机器人。
|
||||
|
||||
### 幸运礼物
|
||||
|
||||
- 每个机器人独立执行。
|
||||
- 从幸运礼物合集中随机选择 1 个礼物。
|
||||
- 当前机器人向同房间所有其他机器人送礼。
|
||||
- 按配置范围随机生成连击次数,例如 `100-10000`。
|
||||
- 连击完成后,按配置间隔范围随机等待,例如 `5-20 秒`,再开始下一轮。
|
||||
|
||||
## 状态管理
|
||||
|
||||
机器人房间支持:
|
||||
|
||||
- 运行中:机器人送礼循环运行。
|
||||
- 已停止:机器人送礼循环停止,配置和房间占用保留。
|
||||
|
||||
后台列表中可使用状态开关快速启动或停止。
|
||||
|
||||
## 数据隔离
|
||||
|
||||
机器人房间遵循以下隔离规则:
|
||||
|
||||
- 机器人只给同房间机器人送礼。
|
||||
- 服务端禁止机器人给真人送礼。
|
||||
- 机器人礼物使用机器人专用金币 `ROBOT_COIN`。
|
||||
- 不消耗真人金币 `COIN`。
|
||||
- 不产生主播钻石和工资周期钻石。
|
||||
- 不写真实礼物墙。
|
||||
- 不参与真实幸运礼物奖池、抽奖、返奖和统计。
|
||||
- 不参与房间排行榜、活动排行榜和结算。
|
||||
|
||||
## 运营建议
|
||||
|
||||
- 单房机器人数量建议从 `6-10` 开始。
|
||||
- 普通礼物频次建议不要过低,避免房间消息过密。
|
||||
- 幸运礼物连击范围建议根据活动氛围配置,测试环境可用较小范围验证。
|
||||
- 礼物合集应选择低价或专门用于展示的礼物,避免用户误解真实返奖。
|
||||
- 机器人房间应优先用于冷启动、活动预热和列表氛围补足。
|
||||
|
||||
## 风险与限制
|
||||
|
||||
- 机器人房间不应和真人主播收益挂钩。
|
||||
- 不建议使用真人主播账号作为机器人账号。
|
||||
- 如果候选机器人不足,创建会失败。
|
||||
- 同一个机器人不能同时被多个 active 机器人房间占用。
|
||||
- 停止房间只停止送礼循环,不删除房间配置。
|
||||
243
docs/机器人房间技术架构文档.md
Normal file
243
docs/机器人房间技术架构文档.md
Normal file
@ -0,0 +1,243 @@
|
||||
# 机器人房间技术架构文档
|
||||
|
||||
## 目标
|
||||
|
||||
机器人房间提供后台配置驱动的内部房间编排能力,支持机器人创建房间、进房、虚拟静音上麦、普通礼物展示、幸运礼物展示,并保证与真实钱包、真实幸运礼物、真实奖池、真实榜单和结算隔离。
|
||||
|
||||
## 模块边界
|
||||
|
||||
### admin-platform
|
||||
|
||||
- 提供机器人房间配置页面。
|
||||
- 调用 admin-server HTTP 接口。
|
||||
- 不直接访问业务服务。
|
||||
|
||||
### admin-server
|
||||
|
||||
- 暴露后台 HTTP API。
|
||||
- 做权限、审计、参数校验和 DTO 转换。
|
||||
- 通过 gRPC client 调用 room-service 和 robot-service。
|
||||
- 通过 wallet-service gRPC 获取礼物配置。
|
||||
- 只读用户库补齐机器人展示资料。
|
||||
|
||||
### robot-service
|
||||
|
||||
- 只拥有机器人账号池事实。
|
||||
- 房间机器人池表:`hyapp_robot.robot_room_robots`。
|
||||
- 不处理进房、麦位、送礼和 IM。
|
||||
|
||||
### room-service
|
||||
|
||||
- 机器人房间配置 owner。
|
||||
- 创建真实房间状态。
|
||||
- 管理机器人房间配置表:`hyapp_room.room_robot_rooms`。
|
||||
- 执行机器人进房、虚拟静音上麦、送礼循环。
|
||||
- 强校验机器人送礼仅限同房间机器人。
|
||||
- 生成机器人幸运礼物展示事件。
|
||||
|
||||
### wallet-service
|
||||
|
||||
- 机器人礼物扣费入口:`DebitRobotGift`。
|
||||
- 使用专用资产 `ROBOT_COIN`。
|
||||
- 自动补足机器人专用金币后扣减。
|
||||
- 不发出真实 `WalletGiftDebited` 事件。
|
||||
- 不写主播周期钻石和真实礼物墙。
|
||||
|
||||
### activity/statistics
|
||||
|
||||
- 消费房间送礼事件时跳过 `is_robot_gift`。
|
||||
- 机器人礼物不进入真实活动、榜单、任务、成长值和统计。
|
||||
|
||||
## 核心数据表
|
||||
|
||||
### `hyapp_robot.robot_room_robots`
|
||||
|
||||
机器人账号池事实表。
|
||||
|
||||
关键字段:
|
||||
|
||||
- `app_code`
|
||||
- `room_scene`
|
||||
- `user_id`
|
||||
- `status`
|
||||
- `last_used_at_ms`
|
||||
- `used_count`
|
||||
|
||||
### `hyapp_room.room_robot_rooms`
|
||||
|
||||
机器人房间配置和占用事实表。
|
||||
|
||||
关键字段:
|
||||
|
||||
- `room_id`
|
||||
- `room_short_id`
|
||||
- `status`
|
||||
- `owner_robot_user_id`
|
||||
- `robot_user_ids_json`
|
||||
- `gift_ids_json`
|
||||
- `lucky_gift_ids_json`
|
||||
- `normal_gift_interval_ms`
|
||||
- `lucky_combo_min`
|
||||
- `lucky_combo_max`
|
||||
- `lucky_pause_min_ms`
|
||||
- `lucky_pause_max_ms`
|
||||
|
||||
## 后台 API
|
||||
|
||||
后台 HTTP:
|
||||
|
||||
- `GET /api/v1/admin/rooms/robot-rooms`
|
||||
- `GET /api/v1/admin/rooms/robot-rooms/available-robots`
|
||||
- `POST /api/v1/admin/rooms/robot-rooms`
|
||||
- `POST /api/v1/admin/rooms/robot-rooms/{room_id}/start`
|
||||
- `POST /api/v1/admin/rooms/robot-rooms/{room_id}/stop`
|
||||
|
||||
room-service gRPC:
|
||||
|
||||
- `AdminListRobotRooms`
|
||||
- `AdminFilterAvailableRoomRobots`
|
||||
- `AdminCreateRobotRoom`
|
||||
- `AdminSetRobotRoomStatus`
|
||||
|
||||
wallet-service gRPC:
|
||||
|
||||
- `DebitRobotGift`
|
||||
|
||||
## 创建流程
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Admin as admin-platform
|
||||
participant AS as admin-server
|
||||
participant RS as room-service
|
||||
participant RB as robot-service
|
||||
participant DB as MySQL
|
||||
|
||||
Admin->>AS: POST /admin/rooms/robot-rooms
|
||||
AS->>RS: AdminCreateRobotRoom
|
||||
RS->>DB: 查询 active 机器人房占用
|
||||
RS->>RS: 从候选机器人随机抽取人数
|
||||
RS->>RS: CreateRoom(robot_room=true)
|
||||
RS->>RS: JoinRoom for robots
|
||||
RS->>RS: RobotVirtualMicUp for robots
|
||||
RS->>DB: 写 room_robot_rooms
|
||||
RS->>RS: start runtime loops
|
||||
RS-->>AS: AdminRobotRoom
|
||||
AS-->>Admin: 机器人房间详情
|
||||
```
|
||||
|
||||
## 运行时循环
|
||||
|
||||
room-service 启动时会扫描所有 active 机器人房间并恢复运行时循环。
|
||||
|
||||
每个 active 机器人房间:
|
||||
|
||||
- 为每个机器人启动普通礼物 loop。
|
||||
- 为每个机器人启动幸运礼物 loop。
|
||||
- stop 后取消对应房间的 context。
|
||||
|
||||
普通礼物 loop:
|
||||
|
||||
1. 等待 `normal_gift_interval_ms`。
|
||||
2. 从 `gift_ids_json` 随机选择礼物。
|
||||
3. 从同房间其他机器人随机选择 1 个目标。
|
||||
4. 调用 `RobotSendGift`。
|
||||
|
||||
幸运礼物 loop:
|
||||
|
||||
1. 从 `lucky_gift_ids_json` 随机选择礼物。
|
||||
2. 随机生成 `lucky_combo_min-lucky_combo_max` 连击次数。
|
||||
3. 当前机器人向同房间其他机器人逐个送礼。
|
||||
4. 使用 `synthetic_lucky_gift=true`,跳过真实抽奖。
|
||||
5. 等待 `lucky_pause_min_ms-lucky_pause_max_ms` 后继续。
|
||||
|
||||
## 服务端安全校验
|
||||
|
||||
room-service 在机器人送礼时强校验:
|
||||
|
||||
- 当前房间必须是机器人房间。
|
||||
- sender 必须在该机器人房间 `robot_user_ids_json` 内。
|
||||
- target 必须在同一机器人房间 `robot_user_ids_json` 内。
|
||||
- sender 不能给真人送礼。
|
||||
- 当前机器人礼物只允许单目标调用。
|
||||
|
||||
## 钱包隔离
|
||||
|
||||
机器人礼物使用 `DebitRobotGift`:
|
||||
|
||||
- 资产类型固定为 `ROBOT_COIN`。
|
||||
- 如果机器人 `ROBOT_COIN` 不足,同一事务内自动补足。
|
||||
- 立即扣减本次礼物价格。
|
||||
- 交易类型为 `robot_gift_debit`。
|
||||
- 不写真实 `gift_debit`。
|
||||
- 不发 `WalletGiftDebited`。
|
||||
- 不写 `user_gift_wall`。
|
||||
- 不写 `host_period_diamond_accounts`。
|
||||
|
||||
## 幸运礼物隔离
|
||||
|
||||
机器人幸运礼物不会调用真实幸运抽奖链路。
|
||||
|
||||
room-service 只生成展示事件:
|
||||
|
||||
- `RoomRobotLuckyGiftDrawn`
|
||||
- 对 IM 客户端映射为 `lucky_gift_drawn`
|
||||
- 标记 `synthetic=true`
|
||||
- 携带机器人房间、送礼人、收礼人、礼物和展示金额字段
|
||||
|
||||
真实奖池、真实流水、真实返奖、真实统计完全不写入。
|
||||
|
||||
## 排行榜与统计隔离
|
||||
|
||||
`RoomGiftSent` 增加:
|
||||
|
||||
- `is_robot_gift`
|
||||
- `synthetic_lucky_gift`
|
||||
|
||||
下游服务看到 `is_robot_gift=true` 时跳过:
|
||||
|
||||
- 活动播报统计
|
||||
- 周星
|
||||
- 房间流水奖励
|
||||
- 每日任务
|
||||
- 成长值
|
||||
- statistics 礼物消费与幸运礼物统计
|
||||
|
||||
room-service 本地仍允许机器人房内:
|
||||
|
||||
- 房间热度增长
|
||||
- 麦位热度增长
|
||||
- 房内展示事件发送
|
||||
|
||||
## 本地接口测试
|
||||
|
||||
测试工具:
|
||||
|
||||
```bash
|
||||
go run ./tools/robot-room-api-test
|
||||
```
|
||||
|
||||
默认依赖:
|
||||
|
||||
- admin-server: `http://127.0.0.1:13100/api/v1`
|
||||
- MySQL: `127.0.0.1:23306`
|
||||
- 管理员账号: `admin / admin123`
|
||||
|
||||
工具会:
|
||||
|
||||
1. 种 12 个房间机器人账号。
|
||||
2. 种 2 个礼物配置和价格。
|
||||
3. 登录 admin-server。
|
||||
4. 调用可用机器人列表接口。
|
||||
5. 创建机器人房间。
|
||||
6. 查询 active 列表。
|
||||
7. 停止、启动、再次停止机器人房间。
|
||||
8. 校验数据库中机器人房间配置和真实 `gift_debit` 隔离。
|
||||
|
||||
## 运维注意事项
|
||||
|
||||
- 多实例 room-service 下,机器人房间 runtime 是进程内循环;重启会从 MySQL active 配置恢复。
|
||||
- 机器人房间配置以 MySQL 为准。
|
||||
- 同一机器人账号不能被多个 active 机器人房间占用。
|
||||
- 停止机器人房间只停止 runtime,不删除房间。
|
||||
- 如果需要彻底释放机器人账号,应增加删除/归档能力后再开放。
|
||||
@ -18,6 +18,7 @@ import (
|
||||
"hyapp-admin-server/internal/config"
|
||||
"hyapp-admin-server/internal/integration/activityclient"
|
||||
"hyapp-admin-server/internal/integration/gameclient"
|
||||
"hyapp-admin-server/internal/integration/robotclient"
|
||||
"hyapp-admin-server/internal/integration/roomclient"
|
||||
"hyapp-admin-server/internal/integration/userclient"
|
||||
"hyapp-admin-server/internal/integration/walletclient"
|
||||
@ -197,6 +198,13 @@ func main() {
|
||||
defer roomConn.Close()
|
||||
roomClient := roomclient.NewGRPC(roomConn)
|
||||
|
||||
robotConn, err := dialBackendGRPC(cfg.RobotService.Addr)
|
||||
if err != nil {
|
||||
fatalRuntime("connect_robot_service_failed", err)
|
||||
}
|
||||
defer robotConn.Close()
|
||||
robotClient := robotclient.NewGRPC(robotConn)
|
||||
|
||||
activityConn, err := dialBackendGRPC(cfg.ActivityService.Addr)
|
||||
if err != nil {
|
||||
fatalRuntime("connect_activity_service_failed", err)
|
||||
@ -279,7 +287,7 @@ func main() {
|
||||
RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
|
||||
RegionBlock: regionblockmodule.New(userDB, auditHandler),
|
||||
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, cfg.WalletService.RequestTimeout, auditHandler),
|
||||
RoomAdmin: roomadminmodule.New(userDB, roomClient, auditHandler),
|
||||
RoomAdmin: roomadminmodule.New(userDB, roomClient, robotClient, auditHandler),
|
||||
RoomRocket: roomrocketmodule.New(roomClient, auditHandler),
|
||||
RoomTurnoverReward: roomturnoverrewardmodule.New(activityclient.NewGRPC(activityConn), auditHandler),
|
||||
Search: searchmodule.New(store),
|
||||
|
||||
@ -47,6 +47,9 @@ wallet_service:
|
||||
room_service:
|
||||
addr: "10.2.1.16:13001"
|
||||
request_timeout: "3s"
|
||||
robot_service:
|
||||
addr: "10.2.1.16:13011"
|
||||
request_timeout: "3s"
|
||||
activity_service:
|
||||
addr: "10.2.1.16:13006"
|
||||
request_timeout: "3s"
|
||||
|
||||
@ -46,6 +46,9 @@ wallet_service:
|
||||
room_service:
|
||||
addr: "127.0.0.1:13001"
|
||||
request_timeout: "3s"
|
||||
robot_service:
|
||||
addr: "127.0.0.1:13011"
|
||||
request_timeout: "3s"
|
||||
activity_service:
|
||||
addr: "127.0.0.1:13006"
|
||||
request_timeout: "3s"
|
||||
|
||||
@ -38,6 +38,7 @@ type Config struct {
|
||||
Jobs JobsConfig `yaml:"jobs"`
|
||||
WalletService WalletServiceConfig `yaml:"wallet_service"`
|
||||
RoomService RoomServiceConfig `yaml:"room_service"`
|
||||
RobotService RobotServiceConfig `yaml:"robot_service"`
|
||||
ActivityService ActivityServiceConfig `yaml:"activity_service"`
|
||||
GameService GameServiceConfig `yaml:"game_service"`
|
||||
StatisticsService StatisticsServiceConfig `yaml:"statistics_service"`
|
||||
@ -64,6 +65,11 @@ type RoomServiceConfig struct {
|
||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||
}
|
||||
|
||||
type RobotServiceConfig struct {
|
||||
Addr string `yaml:"addr"`
|
||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||
}
|
||||
|
||||
type ActivityServiceConfig struct {
|
||||
Addr string `yaml:"addr"`
|
||||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||||
@ -168,6 +174,10 @@ func Default() Config {
|
||||
Addr: "127.0.0.1:13001",
|
||||
RequestTimeout: 3 * time.Second,
|
||||
},
|
||||
RobotService: RobotServiceConfig{
|
||||
Addr: "127.0.0.1:13011",
|
||||
RequestTimeout: 3 * time.Second,
|
||||
},
|
||||
ActivityService: ActivityServiceConfig{
|
||||
Addr: "127.0.0.1:13006",
|
||||
RequestTimeout: 3 * time.Second,
|
||||
@ -281,6 +291,13 @@ func (cfg *Config) Normalize() {
|
||||
if cfg.RoomService.RequestTimeout <= 0 {
|
||||
cfg.RoomService.RequestTimeout = 3 * time.Second
|
||||
}
|
||||
cfg.RobotService.Addr = strings.TrimSpace(cfg.RobotService.Addr)
|
||||
if cfg.RobotService.Addr == "" {
|
||||
cfg.RobotService.Addr = "127.0.0.1:13011"
|
||||
}
|
||||
if cfg.RobotService.RequestTimeout <= 0 {
|
||||
cfg.RobotService.RequestTimeout = 3 * time.Second
|
||||
}
|
||||
cfg.ActivityService.Addr = strings.TrimSpace(cfg.ActivityService.Addr)
|
||||
if cfg.ActivityService.RequestTimeout <= 0 {
|
||||
cfg.ActivityService.RequestTimeout = 3 * time.Second
|
||||
@ -394,6 +411,12 @@ func (cfg Config) Validate() error {
|
||||
if cfg.RoomService.RequestTimeout <= 0 {
|
||||
return errors.New("room_service.request_timeout must be greater than 0")
|
||||
}
|
||||
if strings.TrimSpace(cfg.RobotService.Addr) == "" {
|
||||
return errors.New("robot_service.addr is required")
|
||||
}
|
||||
if cfg.RobotService.RequestTimeout <= 0 {
|
||||
return errors.New("robot_service.request_timeout must be greater than 0")
|
||||
}
|
||||
if strings.TrimSpace(cfg.ActivityService.Addr) == "" {
|
||||
return errors.New("activity_service.addr is required")
|
||||
}
|
||||
|
||||
83
server/admin/internal/integration/robotclient/client.go
Normal file
83
server/admin/internal/integration/robotclient/client.go
Normal file
@ -0,0 +1,83 @@
|
||||
package robotclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
robotv1 "hyapp.local/api/proto/robot/v1"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type Client interface {
|
||||
ListRoomRobots(ctx context.Context, req ListRoomRobotsRequest) (ListRoomRobotsResult, error)
|
||||
}
|
||||
|
||||
type Robot struct {
|
||||
UserID int64
|
||||
Status string
|
||||
RoomScene string
|
||||
CreatedAtMS int64
|
||||
UpdatedAtMS int64
|
||||
LastUsedAtMS int64
|
||||
UsedCount int64
|
||||
}
|
||||
|
||||
type ListRoomRobotsRequest struct {
|
||||
RoomScene string
|
||||
Status string
|
||||
PageSize int32
|
||||
Cursor string
|
||||
}
|
||||
|
||||
type ListRoomRobotsResult struct {
|
||||
Robots []Robot
|
||||
NextCursor string
|
||||
ServerTimeMS int64
|
||||
}
|
||||
|
||||
type GRPCClient struct {
|
||||
client robotv1.RoomRobotServiceClient
|
||||
}
|
||||
|
||||
func NewGRPC(conn grpc.ClientConnInterface) *GRPCClient {
|
||||
return &GRPCClient{client: robotv1.NewRoomRobotServiceClient(conn)}
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListRoomRobots(ctx context.Context, req ListRoomRobotsRequest) (ListRoomRobotsResult, error) {
|
||||
if c == nil || c.client == nil {
|
||||
return ListRoomRobotsResult{}, fmt.Errorf("robot service client is not configured")
|
||||
}
|
||||
resp, err := c.client.ListRoomRobots(ctx, &robotv1.ListRoomRobotsRequest{
|
||||
Meta: &robotv1.RequestMeta{
|
||||
RequestId: fmt.Sprintf("admin-room-robots-%d", time.Now().UnixMilli()),
|
||||
Caller: "admin-server",
|
||||
AppCode: appctx.FromContext(ctx),
|
||||
SentAtMs: time.Now().UnixMilli(),
|
||||
ActorUserId: 0,
|
||||
},
|
||||
RoomScene: strings.TrimSpace(req.RoomScene),
|
||||
Status: strings.TrimSpace(req.Status),
|
||||
PageSize: req.PageSize,
|
||||
Cursor: strings.TrimSpace(req.Cursor),
|
||||
})
|
||||
if err != nil {
|
||||
return ListRoomRobotsResult{}, err
|
||||
}
|
||||
items := make([]Robot, 0, len(resp.GetRobots()))
|
||||
for _, item := range resp.GetRobots() {
|
||||
items = append(items, Robot{
|
||||
UserID: item.GetUserId(),
|
||||
Status: item.GetStatus(),
|
||||
RoomScene: item.GetRoomScene(),
|
||||
CreatedAtMS: item.GetCreatedAtMs(),
|
||||
UpdatedAtMS: item.GetUpdatedAtMs(),
|
||||
LastUsedAtMS: item.GetLastUsedAtMs(),
|
||||
UsedCount: item.GetUsedCount(),
|
||||
})
|
||||
}
|
||||
return ListRoomRobotsResult{Robots: items, NextCursor: resp.GetNextCursor(), ServerTimeMS: resp.GetServerTimeMs()}, nil
|
||||
}
|
||||
@ -24,6 +24,10 @@ type Client interface {
|
||||
ListRoomPins(ctx context.Context, req ListRoomPinsRequest) (ListRoomPinsResult, error)
|
||||
CreateRoomPin(ctx context.Context, req CreateRoomPinRequest) (RoomPin, error)
|
||||
CancelRoomPin(ctx context.Context, req CancelRoomPinRequest) (RoomPin, error)
|
||||
ListRobotRooms(ctx context.Context, req ListRobotRoomsRequest) (ListRobotRoomsResult, error)
|
||||
CreateRobotRoom(ctx context.Context, req CreateRobotRoomRequest) (RobotRoom, error)
|
||||
SetRobotRoomStatus(ctx context.Context, req SetRobotRoomStatusRequest) (RobotRoom, error)
|
||||
FilterAvailableRoomRobots(ctx context.Context, userIDs []int64) (FilterAvailableRoomRobotsResult, error)
|
||||
CloseRoom(ctx context.Context, req CloseRoomRequest) (*CloseRoomResult, error)
|
||||
ReopenRoom(ctx context.Context, req ReopenRoomRequest) (*CloseRoomResult, error)
|
||||
}
|
||||
@ -214,6 +218,66 @@ type CancelRoomPinRequest struct {
|
||||
AdminID uint64
|
||||
}
|
||||
|
||||
type RobotRoomGiftRule struct {
|
||||
GiftIDs []string
|
||||
LuckyGiftIDs []string
|
||||
NormalGiftIntervalMS int64
|
||||
LuckyComboMin int64
|
||||
LuckyComboMax int64
|
||||
LuckyPauseMinMS int64
|
||||
LuckyPauseMaxMS int64
|
||||
}
|
||||
|
||||
type RobotRoom struct {
|
||||
AppCode string
|
||||
RoomID string
|
||||
RoomShortID string
|
||||
Title string
|
||||
CoverURL string
|
||||
VisibleRegionID int64
|
||||
Status string
|
||||
OwnerRobotUserID int64
|
||||
RobotUserIDs []int64
|
||||
GiftRule RobotRoomGiftRule
|
||||
CreatedByAdminID uint64
|
||||
CreatedAtMS int64
|
||||
UpdatedAtMS int64
|
||||
}
|
||||
|
||||
type ListRobotRoomsRequest struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Status string
|
||||
}
|
||||
|
||||
type ListRobotRoomsResult struct {
|
||||
Rooms []RobotRoom
|
||||
Total int64
|
||||
}
|
||||
|
||||
type CreateRobotRoomRequest struct {
|
||||
OwnerRobotUserID int64
|
||||
CandidateRobotUserIDs []int64
|
||||
MinRobotCount int32
|
||||
MaxRobotCount int32
|
||||
RoomName string
|
||||
RoomAvatar string
|
||||
VisibleRegionID int64
|
||||
GiftRule RobotRoomGiftRule
|
||||
AdminID uint64
|
||||
}
|
||||
|
||||
type SetRobotRoomStatusRequest struct {
|
||||
RoomID string
|
||||
Status string
|
||||
AdminID uint64
|
||||
}
|
||||
|
||||
type FilterAvailableRoomRobotsResult struct {
|
||||
AvailableUserIDs []int64
|
||||
OccupiedUserIDs []int64
|
||||
}
|
||||
|
||||
type CloseRoomRequest struct {
|
||||
RequestID string
|
||||
RoomID string
|
||||
@ -400,6 +464,68 @@ func (c *GRPCClient) CancelRoomPin(ctx context.Context, req CancelRoomPinRequest
|
||||
return roomPinFromProto(resp.GetPin()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) ListRobotRooms(ctx context.Context, req ListRobotRoomsRequest) (ListRobotRoomsResult, error) {
|
||||
resp, err := c.queryClient.AdminListRobotRooms(ctx, &roomv1.AdminListRobotRoomsRequest{
|
||||
Meta: requestMeta(ctx, "", 0, "admin-list-robot-rooms"),
|
||||
Page: int32(req.Page),
|
||||
PageSize: int32(req.PageSize),
|
||||
Status: strings.TrimSpace(req.Status),
|
||||
})
|
||||
if err != nil {
|
||||
return ListRobotRoomsResult{}, err
|
||||
}
|
||||
items := make([]RobotRoom, 0, len(resp.GetRooms()))
|
||||
for _, item := range resp.GetRooms() {
|
||||
items = append(items, robotRoomFromProto(item))
|
||||
}
|
||||
return ListRobotRoomsResult{Rooms: items, Total: resp.GetTotal()}, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) CreateRobotRoom(ctx context.Context, req CreateRobotRoomRequest) (RobotRoom, error) {
|
||||
resp, err := c.client.AdminCreateRobotRoom(ctx, &roomv1.AdminCreateRobotRoomRequest{
|
||||
Meta: requestMeta(ctx, "", int64(req.AdminID), "admin-create-robot-room"),
|
||||
OwnerRobotUserId: req.OwnerRobotUserID,
|
||||
CandidateRobotUserIds: append([]int64(nil), req.CandidateRobotUserIDs...),
|
||||
MinRobotCount: req.MinRobotCount,
|
||||
MaxRobotCount: req.MaxRobotCount,
|
||||
RoomName: strings.TrimSpace(req.RoomName),
|
||||
RoomAvatar: strings.TrimSpace(req.RoomAvatar),
|
||||
VisibleRegionId: req.VisibleRegionID,
|
||||
GiftRule: robotRoomGiftRuleToProto(req.GiftRule),
|
||||
AdminId: req.AdminID,
|
||||
})
|
||||
if err != nil {
|
||||
return RobotRoom{}, err
|
||||
}
|
||||
return robotRoomFromProto(resp.GetRoom()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) SetRobotRoomStatus(ctx context.Context, req SetRobotRoomStatusRequest) (RobotRoom, error) {
|
||||
resp, err := c.client.AdminSetRobotRoomStatus(ctx, &roomv1.AdminSetRobotRoomStatusRequest{
|
||||
Meta: requestMeta(ctx, strings.TrimSpace(req.RoomID), int64(req.AdminID), "admin-set-robot-room-status"),
|
||||
Status: strings.TrimSpace(req.Status),
|
||||
AdminId: req.AdminID,
|
||||
})
|
||||
if err != nil {
|
||||
return RobotRoom{}, err
|
||||
}
|
||||
return robotRoomFromProto(resp.GetRoom()), nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) FilterAvailableRoomRobots(ctx context.Context, userIDs []int64) (FilterAvailableRoomRobotsResult, error) {
|
||||
resp, err := c.queryClient.AdminFilterAvailableRoomRobots(ctx, &roomv1.AdminFilterAvailableRoomRobotsRequest{
|
||||
Meta: requestMeta(ctx, "", 0, "admin-filter-available-room-robots"),
|
||||
UserIds: append([]int64(nil), userIDs...),
|
||||
})
|
||||
if err != nil {
|
||||
return FilterAvailableRoomRobotsResult{}, err
|
||||
}
|
||||
return FilterAvailableRoomRobotsResult{
|
||||
AvailableUserIDs: append([]int64(nil), resp.GetAvailableUserIds()...),
|
||||
OccupiedUserIDs: append([]int64(nil), resp.GetOccupiedUserIds()...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *GRPCClient) CloseRoom(ctx context.Context, req CloseRoomRequest) (*CloseRoomResult, error) {
|
||||
return c.setRoomLifecycle(ctx, req.RequestID, req.RoomID, req.ActorUserID, firstNonEmpty(req.Reason, "admin_closed"))
|
||||
}
|
||||
@ -560,6 +686,54 @@ func roomPinFromProto(input *roomv1.AdminRoomPin) RoomPin {
|
||||
}
|
||||
}
|
||||
|
||||
func robotRoomFromProto(input *roomv1.AdminRobotRoom) RobotRoom {
|
||||
if input == nil {
|
||||
return RobotRoom{}
|
||||
}
|
||||
return RobotRoom{
|
||||
AppCode: input.GetAppCode(),
|
||||
RoomID: input.GetRoomId(),
|
||||
RoomShortID: input.GetRoomShortId(),
|
||||
Title: input.GetTitle(),
|
||||
CoverURL: input.GetCoverUrl(),
|
||||
VisibleRegionID: input.GetVisibleRegionId(),
|
||||
Status: input.GetStatus(),
|
||||
OwnerRobotUserID: input.GetOwnerRobotUserId(),
|
||||
RobotUserIDs: append([]int64(nil), input.GetRobotUserIds()...),
|
||||
GiftRule: robotRoomGiftRuleFromProto(input.GetGiftRule()),
|
||||
CreatedByAdminID: input.GetCreatedByAdminId(),
|
||||
CreatedAtMS: input.GetCreatedAtMs(),
|
||||
UpdatedAtMS: input.GetUpdatedAtMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func robotRoomGiftRuleFromProto(input *roomv1.AdminRobotRoomGiftRule) RobotRoomGiftRule {
|
||||
if input == nil {
|
||||
return RobotRoomGiftRule{}
|
||||
}
|
||||
return RobotRoomGiftRule{
|
||||
GiftIDs: append([]string(nil), input.GetGiftIds()...),
|
||||
LuckyGiftIDs: append([]string(nil), input.GetLuckyGiftIds()...),
|
||||
NormalGiftIntervalMS: input.GetNormalGiftIntervalMs(),
|
||||
LuckyComboMin: input.GetLuckyComboMin(),
|
||||
LuckyComboMax: input.GetLuckyComboMax(),
|
||||
LuckyPauseMinMS: input.GetLuckyPauseMinMs(),
|
||||
LuckyPauseMaxMS: input.GetLuckyPauseMaxMs(),
|
||||
}
|
||||
}
|
||||
|
||||
func robotRoomGiftRuleToProto(input RobotRoomGiftRule) *roomv1.AdminRobotRoomGiftRule {
|
||||
return &roomv1.AdminRobotRoomGiftRule{
|
||||
GiftIds: append([]string(nil), input.GiftIDs...),
|
||||
LuckyGiftIds: append([]string(nil), input.LuckyGiftIDs...),
|
||||
NormalGiftIntervalMs: input.NormalGiftIntervalMS,
|
||||
LuckyComboMin: input.LuckyComboMin,
|
||||
LuckyComboMax: input.LuckyComboMax,
|
||||
LuckyPauseMinMs: input.LuckyPauseMinMS,
|
||||
LuckyPauseMaxMs: input.LuckyPauseMaxMS,
|
||||
}
|
||||
}
|
||||
|
||||
func roomRocketConfigToProto(input RoomRocketConfig) *roomv1.AdminRoomRocketConfig {
|
||||
return &roomv1.AdminRoomRocketConfig{
|
||||
AppCode: input.AppCode,
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hyapp-admin-server/internal/integration/robotclient"
|
||||
"hyapp-admin-server/internal/integration/roomclient"
|
||||
"hyapp-admin-server/internal/middleware"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
@ -19,8 +20,8 @@ type Handler struct {
|
||||
audit shared.OperationLogger
|
||||
}
|
||||
|
||||
func New(userDB *sql.DB, roomClient roomclient.Client, audit shared.OperationLogger) *Handler {
|
||||
return &Handler{service: NewService(userDB, roomClient), audit: audit}
|
||||
func New(userDB *sql.DB, roomClient roomclient.Client, robotClient robotclient.Client, audit shared.OperationLogger) *Handler {
|
||||
return &Handler{service: NewService(userDB, roomClient, robotClient), audit: audit}
|
||||
}
|
||||
|
||||
func (h *Handler) ListRooms(c *gin.Context) {
|
||||
@ -78,6 +79,48 @@ func (h *Handler) CancelRoomPin(c *gin.Context) {
|
||||
response.OK(c, pin)
|
||||
}
|
||||
|
||||
func (h *Handler) ListRobotRooms(c *gin.Context) {
|
||||
query := parseRobotRoomListQuery(c)
|
||||
items, total, err := h.service.ListRobotRooms(c.Request.Context(), query)
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取机器人房间列表失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
|
||||
}
|
||||
|
||||
func (h *Handler) ListAvailableRoomRobots(c *gin.Context) {
|
||||
items, err := h.service.ListAvailableRoomRobots(c.Request.Context())
|
||||
if err != nil {
|
||||
response.ServerError(c, "获取可用机器人失败")
|
||||
return
|
||||
}
|
||||
response.OK(c, items)
|
||||
}
|
||||
|
||||
func (h *Handler) CreateRobotRoom(c *gin.Context) {
|
||||
var req createRobotRoomRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "机器人房间参数不正确")
|
||||
return
|
||||
}
|
||||
room, err := h.service.CreateRobotRoom(c.Request.Context(), req, shared.ActorFromContext(c))
|
||||
if err != nil {
|
||||
writeMutationError(c, err, "创建机器人房间失败")
|
||||
return
|
||||
}
|
||||
writeRoomAuditLog(c, h.audit, "create-robot-room", "room_robot_rooms", room.RoomID, "success", "create robot room")
|
||||
response.Created(c, room)
|
||||
}
|
||||
|
||||
func (h *Handler) StartRobotRoom(c *gin.Context) {
|
||||
h.setRobotRoomStatus(c, "active", "start-robot-room", "启动机器人房间失败")
|
||||
}
|
||||
|
||||
func (h *Handler) StopRobotRoom(c *gin.Context) {
|
||||
h.setRobotRoomStatus(c, "stopped", "stop-robot-room", "停止机器人房间失败")
|
||||
}
|
||||
|
||||
func (h *Handler) GetRoomConfig(c *gin.Context) {
|
||||
config, err := h.service.GetRoomConfig(c.Request.Context())
|
||||
if err != nil {
|
||||
@ -134,6 +177,20 @@ func (h *Handler) DeleteRoom(c *gin.Context) {
|
||||
response.OK(c, gin.H{"deleted": true})
|
||||
}
|
||||
|
||||
func (h *Handler) setRobotRoomStatus(c *gin.Context, status string, action string, fallback string) {
|
||||
roomID, ok := parseRoomID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
room, err := h.service.SetRobotRoomStatus(c.Request.Context(), roomID, status, shared.ActorFromContext(c))
|
||||
if err != nil {
|
||||
writeMutationError(c, err, fallback)
|
||||
return
|
||||
}
|
||||
writeRoomAuditLog(c, h.audit, action, "room_robot_rooms", roomID, "success", action)
|
||||
response.OK(c, room)
|
||||
}
|
||||
|
||||
func parseListQuery(c *gin.Context) (listQuery, bool) {
|
||||
options := shared.ListOptions(c)
|
||||
query := listQuery{
|
||||
@ -168,6 +225,15 @@ func parseRoomPinListQuery(c *gin.Context) (roomPinListQuery, bool) {
|
||||
}), true
|
||||
}
|
||||
|
||||
func parseRobotRoomListQuery(c *gin.Context) robotRoomListQuery {
|
||||
options := shared.ListOptions(c)
|
||||
return robotRoomListQuery{
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
Status: options.Status,
|
||||
}
|
||||
}
|
||||
|
||||
func firstQueryValue(c *gin.Context, names ...string) string {
|
||||
for _, name := range names {
|
||||
if value := strings.TrimSpace(c.Query(name)); value != "" {
|
||||
|
||||
@ -19,6 +19,29 @@ type roomPinListQuery struct {
|
||||
Status string
|
||||
}
|
||||
|
||||
type robotRoomListQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Status string
|
||||
}
|
||||
|
||||
type createRobotRoomRequest struct {
|
||||
OwnerRobotUserID int64 `json:"ownerRobotUserId"`
|
||||
CandidateRobotUserIDs []int64 `json:"candidateRobotUserIds"`
|
||||
MinRobotCount int32 `json:"minRobotCount"`
|
||||
MaxRobotCount int32 `json:"maxRobotCount"`
|
||||
RoomName string `json:"roomName"`
|
||||
RoomAvatar string `json:"roomAvatar"`
|
||||
VisibleRegionID int64 `json:"visibleRegionId"`
|
||||
GiftIDs []string `json:"giftIds"`
|
||||
LuckyGiftIDs []string `json:"luckyGiftIds"`
|
||||
NormalGiftIntervalMS int64 `json:"normalGiftIntervalMs"`
|
||||
LuckyComboMin int64 `json:"luckyComboMin"`
|
||||
LuckyComboMax int64 `json:"luckyComboMax"`
|
||||
LuckyPauseMinMS int64 `json:"luckyPauseMinMs"`
|
||||
LuckyPauseMaxMS int64 `json:"luckyPauseMaxMs"`
|
||||
}
|
||||
|
||||
type createRoomPinRequest struct {
|
||||
DurationDays int64 `json:"durationDays"`
|
||||
ExpiresAtMS int64 `json:"expiresAtMs"`
|
||||
|
||||
300
server/admin/internal/modules/roomadmin/robot_room.go
Normal file
300
server/admin/internal/modules/roomadmin/robot_room.go
Normal file
@ -0,0 +1,300 @@
|
||||
package roomadmin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hyapp-admin-server/internal/integration/robotclient"
|
||||
"hyapp-admin-server/internal/integration/roomclient"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
)
|
||||
|
||||
const defaultRoomRobotScene = "voice"
|
||||
|
||||
type RobotRoom struct {
|
||||
AppCode string `json:"appCode"`
|
||||
RoomID string `json:"roomId"`
|
||||
RoomShortID string `json:"roomShortId"`
|
||||
Title string `json:"title"`
|
||||
CoverURL string `json:"coverUrl"`
|
||||
VisibleRegionID int64 `json:"visibleRegionId"`
|
||||
RegionName string `json:"regionName"`
|
||||
Status string `json:"status"`
|
||||
OwnerRobotUserID string `json:"ownerRobotUserId"`
|
||||
Owner RoomOwner `json:"owner"`
|
||||
RobotUserIDs []string `json:"robotUserIds"`
|
||||
Robots []RoomOwner `json:"robots"`
|
||||
GiftRule RobotGiftRule `json:"giftRule"`
|
||||
CreatedByAdminID uint64 `json:"createdByAdminId"`
|
||||
CreatedAtMS int64 `json:"createdAtMs"`
|
||||
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
type RobotGiftRule struct {
|
||||
GiftIDs []string `json:"giftIds"`
|
||||
LuckyGiftIDs []string `json:"luckyGiftIds"`
|
||||
NormalGiftIntervalMS int64 `json:"normalGiftIntervalMs"`
|
||||
LuckyComboMin int64 `json:"luckyComboMin"`
|
||||
LuckyComboMax int64 `json:"luckyComboMax"`
|
||||
LuckyPauseMinMS int64 `json:"luckyPauseMinMs"`
|
||||
LuckyPauseMaxMS int64 `json:"luckyPauseMaxMs"`
|
||||
}
|
||||
|
||||
type AvailableRoomRobot struct {
|
||||
UserID string `json:"userId"`
|
||||
Status string `json:"status"`
|
||||
RoomScene string `json:"roomScene"`
|
||||
LastUsedAtMS int64 `json:"lastUsedAtMs"`
|
||||
UsedCount int64 `json:"usedCount"`
|
||||
User RoomOwner `json:"user"`
|
||||
}
|
||||
|
||||
func (s *Service) ListRobotRooms(ctx context.Context, query robotRoomListQuery) ([]RobotRoom, int64, error) {
|
||||
if s.roomClient == nil {
|
||||
return nil, 0, fmt.Errorf("room service client is not configured")
|
||||
}
|
||||
if query.Page <= 0 {
|
||||
query.Page = 1
|
||||
}
|
||||
if query.PageSize <= 0 || query.PageSize > 100 {
|
||||
query.PageSize = 50
|
||||
}
|
||||
result, err := s.roomClient.ListRobotRooms(ctx, roomclient.ListRobotRoomsRequest{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
Status: normalizeRobotRoomListStatus(query.Status),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items := robotRoomsFromClient(result.Rooms)
|
||||
if err := s.fillRobotRoomDetails(ctx, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, result.Total, nil
|
||||
}
|
||||
|
||||
func normalizeRobotRoomListStatus(status string) string {
|
||||
switch strings.TrimSpace(status) {
|
||||
case "active", "stopped":
|
||||
return strings.TrimSpace(status)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) ListAvailableRoomRobots(ctx context.Context) ([]AvailableRoomRobot, error) {
|
||||
if s.robotClient == nil || s.roomClient == nil {
|
||||
return nil, fmt.Errorf("robot or room service client is not configured")
|
||||
}
|
||||
result, err := s.robotClient.ListRoomRobots(ctx, robotclient.ListRoomRobotsRequest{
|
||||
RoomScene: defaultRoomRobotScene,
|
||||
Status: "active",
|
||||
PageSize: 200,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userIDs := make([]int64, 0, len(result.Robots))
|
||||
for _, item := range result.Robots {
|
||||
if item.UserID > 0 {
|
||||
userIDs = append(userIDs, item.UserID)
|
||||
}
|
||||
}
|
||||
available, err := s.roomClient.FilterAvailableRoomRobots(ctx, userIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
allowed := make(map[int64]bool, len(available.AvailableUserIDs))
|
||||
for _, userID := range available.AvailableUserIDs {
|
||||
allowed[userID] = true
|
||||
}
|
||||
owners, err := s.queryRoomOwners(ctx, available.AvailableUserIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]AvailableRoomRobot, 0, len(available.AvailableUserIDs))
|
||||
for _, robot := range result.Robots {
|
||||
if !allowed[robot.UserID] {
|
||||
continue
|
||||
}
|
||||
items = append(items, AvailableRoomRobot{
|
||||
UserID: strconv.FormatInt(robot.UserID, 10),
|
||||
Status: robot.Status,
|
||||
RoomScene: robot.RoomScene,
|
||||
LastUsedAtMS: robot.LastUsedAtMS,
|
||||
UsedCount: robot.UsedCount,
|
||||
User: owners[robot.UserID],
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreateRobotRoom(ctx context.Context, req createRobotRoomRequest, actor shared.Actor) (RobotRoom, error) {
|
||||
if s.roomClient == nil {
|
||||
return RobotRoom{}, fmt.Errorf("room service client is not configured")
|
||||
}
|
||||
if _, err := normalizeCreateRobotRoomRequest(req); err != nil {
|
||||
return RobotRoom{}, err
|
||||
}
|
||||
created, err := s.roomClient.CreateRobotRoom(ctx, roomclient.CreateRobotRoomRequest{
|
||||
OwnerRobotUserID: req.OwnerRobotUserID,
|
||||
CandidateRobotUserIDs: req.CandidateRobotUserIDs,
|
||||
MinRobotCount: req.MinRobotCount,
|
||||
MaxRobotCount: req.MaxRobotCount,
|
||||
RoomName: req.RoomName,
|
||||
RoomAvatar: req.RoomAvatar,
|
||||
VisibleRegionID: req.VisibleRegionID,
|
||||
GiftRule: roomclient.RobotRoomGiftRule{
|
||||
GiftIDs: req.GiftIDs,
|
||||
LuckyGiftIDs: req.LuckyGiftIDs,
|
||||
NormalGiftIntervalMS: req.NormalGiftIntervalMS,
|
||||
LuckyComboMin: req.LuckyComboMin,
|
||||
LuckyComboMax: req.LuckyComboMax,
|
||||
LuckyPauseMinMS: req.LuckyPauseMinMS,
|
||||
LuckyPauseMaxMS: req.LuckyPauseMaxMS,
|
||||
},
|
||||
AdminID: uint64(actor.UserID),
|
||||
})
|
||||
if err != nil {
|
||||
return RobotRoom{}, mapRoomClientError(err)
|
||||
}
|
||||
items := []RobotRoom{robotRoomFromClient(created)}
|
||||
if err := s.fillRobotRoomDetails(ctx, items); err != nil {
|
||||
return RobotRoom{}, err
|
||||
}
|
||||
return items[0], nil
|
||||
}
|
||||
|
||||
func (s *Service) SetRobotRoomStatus(ctx context.Context, roomID string, status string, actor shared.Actor) (RobotRoom, error) {
|
||||
if s.roomClient == nil {
|
||||
return RobotRoom{}, fmt.Errorf("room service client is not configured")
|
||||
}
|
||||
roomID = strings.TrimSpace(roomID)
|
||||
status = strings.TrimSpace(status)
|
||||
if roomID == "" || (status != "active" && status != "stopped") {
|
||||
return RobotRoom{}, fmt.Errorf("%w: 机器人房间状态参数不正确", ErrInvalidArgument)
|
||||
}
|
||||
item, err := s.roomClient.SetRobotRoomStatus(ctx, roomclient.SetRobotRoomStatusRequest{
|
||||
RoomID: roomID,
|
||||
Status: status,
|
||||
AdminID: uint64(actor.UserID),
|
||||
})
|
||||
if err != nil {
|
||||
return RobotRoom{}, mapRoomClientError(err)
|
||||
}
|
||||
items := []RobotRoom{robotRoomFromClient(item)}
|
||||
if err := s.fillRobotRoomDetails(ctx, items); err != nil {
|
||||
return RobotRoom{}, err
|
||||
}
|
||||
return items[0], nil
|
||||
}
|
||||
|
||||
func normalizeCreateRobotRoomRequest(req createRobotRoomRequest) (createRobotRoomRequest, error) {
|
||||
if req.OwnerRobotUserID <= 0 {
|
||||
return req, fmt.Errorf("%w: 请选择房主机器人", ErrInvalidArgument)
|
||||
}
|
||||
if req.MinRobotCount <= 0 || req.MaxRobotCount < req.MinRobotCount {
|
||||
return req, fmt.Errorf("%w: 机器人数量范围不正确", ErrInvalidArgument)
|
||||
}
|
||||
if len(req.CandidateRobotUserIDs) == 0 {
|
||||
return req, fmt.Errorf("%w: 请选择候选机器人", ErrInvalidArgument)
|
||||
}
|
||||
if len(req.GiftIDs) == 0 {
|
||||
return req, fmt.Errorf("%w: 请选择普通礼物合集", ErrInvalidArgument)
|
||||
}
|
||||
if len(req.LuckyGiftIDs) == 0 {
|
||||
return req, fmt.Errorf("%w: 请选择幸运礼物合集", ErrInvalidArgument)
|
||||
}
|
||||
if req.NormalGiftIntervalMS <= 0 {
|
||||
return req, fmt.Errorf("%w: 普通礼物频次不正确", ErrInvalidArgument)
|
||||
}
|
||||
if req.LuckyComboMin <= 0 || req.LuckyComboMax < req.LuckyComboMin {
|
||||
return req, fmt.Errorf("%w: 幸运礼物连击范围不正确", ErrInvalidArgument)
|
||||
}
|
||||
if req.LuckyPauseMinMS <= 0 || req.LuckyPauseMaxMS < req.LuckyPauseMinMS {
|
||||
return req, fmt.Errorf("%w: 幸运礼物间隔范围不正确", ErrInvalidArgument)
|
||||
}
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func robotRoomsFromClient(items []roomclient.RobotRoom) []RobotRoom {
|
||||
out := make([]RobotRoom, 0, len(items))
|
||||
for _, item := range items {
|
||||
out = append(out, robotRoomFromClient(item))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func robotRoomFromClient(item roomclient.RobotRoom) RobotRoom {
|
||||
robotUserIDs := make([]string, 0, len(item.RobotUserIDs))
|
||||
for _, userID := range item.RobotUserIDs {
|
||||
robotUserIDs = append(robotUserIDs, strconv.FormatInt(userID, 10))
|
||||
}
|
||||
return RobotRoom{
|
||||
AppCode: item.AppCode,
|
||||
RoomID: item.RoomID,
|
||||
RoomShortID: item.RoomShortID,
|
||||
Title: item.Title,
|
||||
CoverURL: item.CoverURL,
|
||||
VisibleRegionID: item.VisibleRegionID,
|
||||
Status: item.Status,
|
||||
OwnerRobotUserID: strconv.FormatInt(item.OwnerRobotUserID, 10),
|
||||
RobotUserIDs: robotUserIDs,
|
||||
GiftRule: RobotGiftRule{
|
||||
GiftIDs: item.GiftRule.GiftIDs,
|
||||
LuckyGiftIDs: item.GiftRule.LuckyGiftIDs,
|
||||
NormalGiftIntervalMS: item.GiftRule.NormalGiftIntervalMS,
|
||||
LuckyComboMin: item.GiftRule.LuckyComboMin,
|
||||
LuckyComboMax: item.GiftRule.LuckyComboMax,
|
||||
LuckyPauseMinMS: item.GiftRule.LuckyPauseMinMS,
|
||||
LuckyPauseMaxMS: item.GiftRule.LuckyPauseMaxMS,
|
||||
},
|
||||
CreatedByAdminID: item.CreatedByAdminID,
|
||||
CreatedAtMS: item.CreatedAtMS,
|
||||
UpdatedAtMS: item.UpdatedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) fillRobotRoomDetails(ctx context.Context, items []RobotRoom) error {
|
||||
if s.userDB == nil || len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
userIDs := make([]int64, 0)
|
||||
regionIDs := make([]int64, 0)
|
||||
for _, item := range items {
|
||||
if ownerID, err := strconv.ParseInt(item.OwnerRobotUserID, 10, 64); err == nil && ownerID > 0 {
|
||||
userIDs = append(userIDs, ownerID)
|
||||
}
|
||||
for _, rawID := range item.RobotUserIDs {
|
||||
if userID, err := strconv.ParseInt(rawID, 10, 64); err == nil && userID > 0 {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
}
|
||||
if item.VisibleRegionID > 0 {
|
||||
regionIDs = append(regionIDs, item.VisibleRegionID)
|
||||
}
|
||||
}
|
||||
owners, err := s.queryRoomOwners(ctx, uniqueInt64s(userIDs))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
regions, err := s.queryRegionNames(ctx, uniqueInt64s(regionIDs))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for index := range items {
|
||||
if ownerID, err := strconv.ParseInt(items[index].OwnerRobotUserID, 10, 64); err == nil {
|
||||
items[index].Owner = owners[ownerID]
|
||||
}
|
||||
for _, rawID := range items[index].RobotUserIDs {
|
||||
if userID, err := strconv.ParseInt(rawID, 10, 64); err == nil {
|
||||
items[index].Robots = append(items[index].Robots, owners[userID])
|
||||
}
|
||||
}
|
||||
items[index].RegionName = regions[items[index].VisibleRegionID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -15,6 +15,11 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
||||
protected.GET("/admin/rooms/pins", middleware.RequirePermission("room-pin:view"), h.ListRoomPins)
|
||||
protected.POST("/admin/rooms/pins", middleware.RequirePermission("room-pin:create"), h.CreateRoomPin)
|
||||
protected.DELETE("/admin/rooms/pins/:pin_id", middleware.RequirePermission("room-pin:cancel"), h.CancelRoomPin)
|
||||
protected.GET("/admin/rooms/robot-rooms", middleware.RequirePermission("room-robot:view"), h.ListRobotRooms)
|
||||
protected.GET("/admin/rooms/robot-rooms/available-robots", middleware.RequirePermission("room-robot:view"), h.ListAvailableRoomRobots)
|
||||
protected.POST("/admin/rooms/robot-rooms", middleware.RequirePermission("room-robot:create"), h.CreateRobotRoom)
|
||||
protected.POST("/admin/rooms/robot-rooms/:room_id/start", middleware.RequirePermission("room-robot:update"), h.StartRobotRoom)
|
||||
protected.POST("/admin/rooms/robot-rooms/:room_id/stop", middleware.RequirePermission("room-robot:update"), h.StopRobotRoom)
|
||||
protected.GET("/admin/rooms/config", middleware.RequirePermission("room-config:view"), h.GetRoomConfig)
|
||||
protected.PUT("/admin/rooms/config", middleware.RequirePermission("room-config:update"), h.UpdateRoomConfig)
|
||||
protected.PATCH("/admin/rooms/:room_id", middleware.RequirePermission("room:update"), h.UpdateRoom)
|
||||
|
||||
@ -12,6 +12,7 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/robotclient"
|
||||
"hyapp-admin-server/internal/integration/roomclient"
|
||||
"hyapp-admin-server/internal/modules/shared"
|
||||
)
|
||||
@ -24,6 +25,7 @@ var (
|
||||
type Service struct {
|
||||
userDB *sql.DB
|
||||
roomClient roomclient.Client
|
||||
robotClient robotclient.Client
|
||||
}
|
||||
|
||||
type RoomOwner struct {
|
||||
@ -58,8 +60,8 @@ type Room struct {
|
||||
VisibleRegionID int64 `json:"visibleRegionId"`
|
||||
}
|
||||
|
||||
func NewService(userDB *sql.DB, roomClient roomclient.Client) *Service {
|
||||
return &Service{userDB: userDB, roomClient: roomClient}
|
||||
func NewService(userDB *sql.DB, roomClient roomclient.Client, robotClient robotclient.Client) *Service {
|
||||
return &Service{userDB: userDB, roomClient: roomClient, robotClient: robotClient}
|
||||
}
|
||||
|
||||
func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64, error) {
|
||||
|
||||
@ -36,6 +36,9 @@ var defaultPermissions = []model.Permission{
|
||||
{Name: "房间置顶取消", Code: "room-pin:cancel", Kind: "button"},
|
||||
{Name: "房间配置查看", Code: "room-config:view", Kind: "menu"},
|
||||
{Name: "房间配置更新", Code: "room-config:update", Kind: "button"},
|
||||
{Name: "机器人房间查看", Code: "room-robot:view", Kind: "menu"},
|
||||
{Name: "机器人房间创建", Code: "room-robot:create", Kind: "button"},
|
||||
{Name: "机器人房间更新", Code: "room-robot:update", Kind: "button"},
|
||||
{Name: "APP 配置查看", Code: "app-config:view", Kind: "menu"},
|
||||
{Name: "APP 配置更新", Code: "app-config:update", Kind: "button"},
|
||||
{Name: "版本管理查看", Code: "app-version:view", Kind: "menu"},
|
||||
@ -255,6 +258,7 @@ func (s *Store) seedMenus() error {
|
||||
{ParentID: &roomsID, Title: "房间列表", Code: "room-list", Path: "/rooms", Icon: "room", PermissionCode: "room:view", Sort: 65, Visible: true},
|
||||
{ParentID: &roomsID, Title: "房间置顶", Code: "room-pins", Path: "/rooms/pins", Icon: "push_pin", PermissionCode: "room-pin:view", Sort: 66, Visible: true},
|
||||
{ParentID: &roomsID, Title: "房间配置", Code: "room-config", Path: "/rooms/config", Icon: "settings", PermissionCode: "room-config:view", Sort: 67, Visible: true},
|
||||
{ParentID: &roomsID, Title: "机器人房间", Code: "room-robots", Path: "/rooms/robots", Icon: "team", PermissionCode: "room-robot:view", Sort: 68, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "H5配置", Code: "app-config-h5", Path: "/app-config/h5", Icon: "settings", PermissionCode: "app-config:view", Sort: 66, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "BANNER配置", Code: "app-config-banners", Path: "/app-config/banners", Icon: "image", PermissionCode: "app-config:view", Sort: 67, Visible: true},
|
||||
{ParentID: &appConfigID, Title: "开屏配置", Code: "app-config-splash-screens", Path: "/app-config/splash-screens", Icon: "image", PermissionCode: "app-config:view", Sort: 68, Visible: true},
|
||||
@ -511,7 +515,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"level-config:view", "level-config:update",
|
||||
"pretty-id:view", "pretty-id:update", "pretty-id:generate", "pretty-id:grant",
|
||||
"region-block:view", "region-block:update",
|
||||
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update",
|
||||
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update", "room-robot:view", "room-robot:create", "room-robot:update",
|
||||
"app-config:view", "app-config:update",
|
||||
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
||||
"resource:view", "resource:create", "resource:update",
|
||||
@ -542,7 +546,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"upload:create",
|
||||
}
|
||||
case "auditor":
|
||||
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "lucky-gift:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view", "role:view", "permission:view", "job:view"}
|
||||
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "lucky-gift:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view", "role:view", "permission:view", "job:view"}
|
||||
case "readonly":
|
||||
return []string{
|
||||
"overview:view",
|
||||
@ -554,6 +558,7 @@ func defaultRolePermissionCodes(code string) []string {
|
||||
"room:view",
|
||||
"room-pin:view",
|
||||
"room-config:view",
|
||||
"room-robot:view",
|
||||
"app-config:view",
|
||||
"app-version:view",
|
||||
"resource:view",
|
||||
@ -605,7 +610,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
||||
case "ops-admin":
|
||||
return []string{
|
||||
"app-user:update", "app-user:status", "app-user:password",
|
||||
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update",
|
||||
"room:view", "room:update", "room:delete", "room-pin:view", "room-pin:create", "room-pin:cancel", "room-config:view", "room-config:update", "room-robot:view", "room-robot:create", "room-robot:update",
|
||||
"app-config:view", "app-config:update",
|
||||
"app-version:view", "app-version:create", "app-version:update", "app-version:delete",
|
||||
"level-config:view", "level-config:update",
|
||||
@ -635,7 +640,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
||||
"weekly-star:view", "weekly-star:create", "weekly-star:update", "weekly-star:settle",
|
||||
}
|
||||
case "auditor", "readonly":
|
||||
return []string{"level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-seller:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "lucky-gift:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view"}
|
||||
return []string{"level-config:view", "pretty-id:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "room-robot:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "host-agency-policy:view", "team-salary-policy:view", "host-salary-settlement:view", "coin-seller:view", "coin-ledger:view", "coin-seller-ledger:view", "coin-adjustment:view", "report:view", "gift-diamond:view", "lucky-gift:view", "payment-bill:view", "payment-third-party:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-rocket:view", "red-packet:view", "cp-config:view", "vip-config:view", "weekly-star:view"}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
41
server/admin/migrations/054_robot_room_navigation.sql
Normal file
41
server/admin/migrations/054_robot_room_navigation.sql
Normal file
@ -0,0 +1,41 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||
|
||||
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
|
||||
('机器人房间查看', 'room-robot:view', 'menu', '', @now_ms, @now_ms),
|
||||
('机器人房间创建', 'room-robot:create', 'button', '', @now_ms, @now_ms),
|
||||
('机器人房间更新', 'room-robot:update', 'button', '', @now_ms, @now_ms)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
kind = VALUES(kind),
|
||||
description = VALUES(description),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
|
||||
SELECT parent.id, '机器人房间', 'room-robots', '/rooms/robots', 'team', 'room-robot:view', 68, TRUE, @now_ms, @now_ms
|
||||
FROM admin_menus parent
|
||||
WHERE parent.code = 'rooms'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
parent_id = VALUES(parent_id),
|
||||
title = VALUES(title),
|
||||
path = VALUES(path),
|
||||
icon = VALUES(icon),
|
||||
permission_code = VALUES(permission_code),
|
||||
sort = VALUES(sort),
|
||||
visible = VALUES(visible),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||
SELECT admin_role.id, admin_permission.id
|
||||
FROM admin_roles admin_role
|
||||
JOIN admin_permissions admin_permission
|
||||
WHERE admin_role.code IN ('platform-admin', 'ops-admin')
|
||||
AND admin_permission.code IN ('room-robot:view', 'room-robot:create', 'room-robot:update');
|
||||
|
||||
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||
SELECT admin_role.id, admin_permission.id
|
||||
FROM admin_roles admin_role
|
||||
JOIN admin_permissions admin_permission
|
||||
WHERE admin_role.code IN ('auditor', 'readonly')
|
||||
AND admin_permission.code = 'room-robot:view';
|
||||
@ -323,6 +323,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
|
||||
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
|
||||
return broadcastdomain.ConsumeRoomEventResult{}, err
|
||||
}
|
||||
if gift.GetIsRobotGift() {
|
||||
return result, nil
|
||||
}
|
||||
if gift.GetVisibleRegionId() <= 0 || !s.shouldBroadcastGift(&gift) {
|
||||
// visible_region_id 来自房间/主播可见区域,不能用客户端当前 IP 或本地时区推断。
|
||||
return result, nil
|
||||
|
||||
@ -257,6 +257,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
|
||||
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if gift.GetIsRobotGift() {
|
||||
return 0, nil
|
||||
}
|
||||
if gift.GetSenderUserId() <= 0 || gift.GetTargetUserId() <= 0 || gift.GetGiftValue() <= 0 {
|
||||
return 0, xerr.New(xerr.InvalidArgument, "room gift event is incomplete")
|
||||
}
|
||||
|
||||
@ -157,6 +157,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
|
||||
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
|
||||
return domain.EventResult{}, err
|
||||
}
|
||||
if gift.GetIsRobotGift() {
|
||||
return domain.EventResult{EventID: envelope.GetEventId(), Status: domain.EventStatusSkipped}, nil
|
||||
}
|
||||
if strings.TrimSpace(envelope.GetRoomId()) == "" || gift.GetCoinSpent() <= 0 {
|
||||
// 缺房间或 0 金币不会形成有效房间贡献;这类事件本身没有可补偿数据,跳过比失败重放更安全。
|
||||
return domain.EventResult{EventID: envelope.GetEventId(), Status: domain.EventStatusSkipped}, nil
|
||||
|
||||
@ -207,6 +207,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
|
||||
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if gift.GetIsRobotGift() {
|
||||
return 0, nil
|
||||
}
|
||||
if gift.GetSenderUserId() <= 0 || gift.GetGiftId() == "" || gift.GetGiftCount() <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@ -163,6 +163,9 @@ func (s *Service) HandleRoomEvent(ctx context.Context, envelope *roomeventsv1.Ev
|
||||
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if gift.GetIsRobotGift() {
|
||||
return 0, nil
|
||||
}
|
||||
if gift.GetSenderUserId() <= 0 || gift.GetGiftId() == "" || gift.GetCoinSpent() <= 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@ -225,6 +225,31 @@ CREATE TABLE IF NOT EXISTS room_rocket_configs (
|
||||
KEY idx_room_rocket_enabled (app_code, enabled)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='语音房火箭后台配置表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS room_robot_rooms (
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||||
room_id VARCHAR(64) NOT NULL COMMENT '机器人房间 ID',
|
||||
room_short_id VARCHAR(32) NOT NULL DEFAULT '' COMMENT '房间短 ID',
|
||||
title VARCHAR(128) NOT NULL DEFAULT '' COMMENT '房间名称',
|
||||
cover_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '房间头像',
|
||||
visible_region_id BIGINT NOT NULL DEFAULT 0 COMMENT '可见区域 ID',
|
||||
status VARCHAR(32) NOT NULL COMMENT '运行状态:active/stopped',
|
||||
owner_robot_user_id BIGINT NOT NULL COMMENT '房主机器人用户 ID',
|
||||
robot_user_ids_json JSON NOT NULL COMMENT '本房间占用的机器人用户 ID 集合',
|
||||
gift_ids_json JSON NOT NULL COMMENT '普通礼物 ID 集合',
|
||||
lucky_gift_ids_json JSON NOT NULL COMMENT '幸运礼物 ID 集合',
|
||||
normal_gift_interval_ms BIGINT NOT NULL COMMENT '普通礼物间隔毫秒',
|
||||
lucky_combo_min BIGINT NOT NULL COMMENT '幸运礼物最小连击次数',
|
||||
lucky_combo_max BIGINT NOT NULL COMMENT '幸运礼物最大连击次数',
|
||||
lucky_pause_min_ms BIGINT NOT NULL COMMENT '幸运礼物连击后最小暂停毫秒',
|
||||
lucky_pause_max_ms BIGINT NOT NULL COMMENT '幸运礼物连击后最大暂停毫秒',
|
||||
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建管理员 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
PRIMARY KEY (app_code, room_id),
|
||||
KEY idx_room_robot_rooms_status (app_code, status, updated_at_ms, room_id),
|
||||
KEY idx_room_robot_rooms_owner (app_code, owner_robot_user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='机器人房间后台配置表';
|
||||
|
||||
INSERT IGNORE INTO room_seat_configs (
|
||||
app_code,
|
||||
allowed_seat_counts,
|
||||
|
||||
@ -391,6 +391,9 @@ func (a *App) Run() error {
|
||||
})
|
||||
})
|
||||
}
|
||||
a.workerWG.Go(func() {
|
||||
a.service.RunRobotRoomRuntimeManager(a.workerCtx, 10*time.Second)
|
||||
})
|
||||
|
||||
err := a.grpcServer.Serve(a.listener)
|
||||
a.health.MarkGRPCStopped()
|
||||
|
||||
@ -16,6 +16,8 @@ type WalletClient interface {
|
||||
DebitGift(ctx context.Context, req *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error)
|
||||
// BatchDebitGift 在 wallet-service 单事务内结算多目标送礼;任一目标失败时整批失败。
|
||||
BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error)
|
||||
// DebitRobotGift 扣机器人专用金币;只用于机器人房间内部展示礼物。
|
||||
DebitRobotGift(ctx context.Context, req *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error)
|
||||
// GrantResourceGroup 发放火箭奖励资源组;命令 ID 必须由 room-service 按火箭结算事实生成。
|
||||
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error)
|
||||
}
|
||||
|
||||
@ -34,6 +34,11 @@ func (c *grpcWalletClient) BatchDebitGift(ctx context.Context, req *walletv1.Bat
|
||||
return c.client.BatchDebitGift(ctx, req)
|
||||
}
|
||||
|
||||
// DebitRobotGift 直接转调 wallet-service 机器人专用金币扣费接口。
|
||||
func (c *grpcWalletClient) DebitRobotGift(ctx context.Context, req *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error) {
|
||||
return c.client.DebitRobotGift(ctx, req)
|
||||
}
|
||||
|
||||
// GrantResourceGroup 直接转调 wallet-service 资源组发放接口。
|
||||
func (c *grpcWalletClient) GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
|
||||
// 火箭奖励以 resource group 为后台配置单位,wallet-service 负责展开资产和权益。
|
||||
|
||||
@ -315,6 +315,36 @@ func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.Room
|
||||
"gift_effect_types": string(effectTypesJSON),
|
||||
}
|
||||
return base, true, nil
|
||||
case "RoomRobotLuckyGiftDrawn":
|
||||
// 机器人幸运礼物只发房间展示协议,不进入 activity-service 真实抽奖和奖池链路。
|
||||
var body roomeventsv1.RoomRobotLuckyGiftDrawn
|
||||
if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil {
|
||||
return tencentim.RoomEvent{}, false, err
|
||||
}
|
||||
base.ActorUserID = body.GetSenderUserId()
|
||||
base.TargetUserID = body.GetTargetUserId()
|
||||
base.GiftValue = body.GetEffectiveRewardCoins()
|
||||
base.Attributes = map[string]string{
|
||||
"event_type": "lucky_gift_drawn",
|
||||
"draw_id": body.GetDrawId(),
|
||||
"command_id": body.GetCommandId(),
|
||||
"sender_user_id": fmt.Sprintf("%d", body.GetSenderUserId()),
|
||||
"target_user_id": fmt.Sprintf("%d", body.GetTargetUserId()),
|
||||
"gift_id": body.GetGiftId(),
|
||||
"gift_count": fmt.Sprintf("%d", body.GetGiftCount()),
|
||||
"pool_id": body.GetPoolId(),
|
||||
"multiplier_ppm": fmt.Sprintf("%d", body.GetMultiplierPpm()),
|
||||
"base_reward_coins": fmt.Sprintf("%d", body.GetBaseRewardCoins()),
|
||||
"room_atmosphere_reward_coins": fmt.Sprintf("%d", body.GetRoomAtmosphereRewardCoins()),
|
||||
"activity_subsidy_coins": fmt.Sprintf("%d", body.GetActivitySubsidyCoins()),
|
||||
"effective_reward_coins": fmt.Sprintf("%d", body.GetEffectiveRewardCoins()),
|
||||
"created_at_ms": fmt.Sprintf("%d", body.GetCreatedAtMs()),
|
||||
"visible_region_id": fmt.Sprintf("%d", body.GetVisibleRegionId()),
|
||||
"is_robot": fmt.Sprintf("%t", body.GetIsRobot()),
|
||||
"synthetic": fmt.Sprintf("%t", body.GetSynthetic()),
|
||||
"reward_status": "granted",
|
||||
}
|
||||
return base, true, nil
|
||||
case "RoomRocketFuelChanged":
|
||||
var body roomeventsv1.RoomRocketFuelChanged
|
||||
if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil {
|
||||
@ -441,6 +471,8 @@ func eventTypeForClient(eventType string) string {
|
||||
return "room_user_unbanned"
|
||||
case "RoomGiftSent":
|
||||
return "room_gift_sent"
|
||||
case "RoomRobotLuckyGiftDrawn":
|
||||
return "lucky_gift_drawn"
|
||||
case "RoomRocketFuelChanged":
|
||||
return "room_rocket_fuel_changed"
|
||||
case "RoomRocketIgnited":
|
||||
|
||||
@ -74,6 +74,10 @@ type CreateRoom struct {
|
||||
RoomDescription string `json:"room_description"`
|
||||
// RoomShortID 是房间短 ID,首版等于创建者当前展示短号。
|
||||
RoomShortID string `json:"room_short_id"`
|
||||
// RobotRoom 标记该房间由内部机器人编排器创建,机器人礼物校验依赖它 fail-close。
|
||||
RobotRoom bool `json:"robot_room,omitempty"`
|
||||
// RobotUserIDs 是该机器人房间允许参与虚拟上麦和机器人送礼的用户集合。
|
||||
RobotUserIDs []int64 `json:"robot_user_ids,omitempty"`
|
||||
}
|
||||
|
||||
// Type 返回命令类型。
|
||||
@ -200,6 +204,17 @@ type MicUp struct {
|
||||
// Type 返回命令类型。
|
||||
func (MicUp) Type() string { return "mic_up" }
|
||||
|
||||
// RobotMicUp 定义机器人虚拟静音占麦请求。
|
||||
// 它不等待 RTC 发流确认,也不会被 pending_publish 超时任务释放。
|
||||
type RobotMicUp struct {
|
||||
Base
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
SeatNo int32 `json:"seat_no"`
|
||||
MicSessionID string `json:"mic_session_id"`
|
||||
}
|
||||
|
||||
func (RobotMicUp) Type() string { return "robot_mic_up" }
|
||||
|
||||
// MicDown 定义下麦请求。
|
||||
type MicDown struct {
|
||||
Base
|
||||
@ -459,6 +474,14 @@ type SendGift struct {
|
||||
RocketPendingLaunches []RocketPendingLaunch `json:"rocket_pending_launches,omitempty"`
|
||||
// RocketCurrentContributions 保存当前等级真实加燃料贡献,恢复时用于继续按本级贡献锁 top1。
|
||||
RocketCurrentContributions []RocketContribution `json:"rocket_current_contributions,omitempty"`
|
||||
// RobotGift 表示本次礼物只服务机器人房间展示;不写真实统计、任务、周星和房间贡献榜。
|
||||
RobotGift bool `json:"robot_gift,omitempty"`
|
||||
// SyntheticLuckyGift 表示幸运礼物只生成展示事件,不调用真实 activity 抽奖和奖池。
|
||||
SyntheticLuckyGift bool `json:"synthetic_lucky_gift,omitempty"`
|
||||
// SyntheticLuckyRewardCoins 是机器人幸运礼物展示用爆奖金币值,不代表真实钱包返奖。
|
||||
SyntheticLuckyRewardCoins int64 `json:"synthetic_lucky_reward_coins,omitempty"`
|
||||
// SyntheticLuckyMultiplierPPM 是机器人幸运礼物展示用倍率。
|
||||
SyntheticLuckyMultiplierPPM int64 `json:"synthetic_lucky_multiplier_ppm,omitempty"`
|
||||
}
|
||||
|
||||
// Type 返回命令类型。
|
||||
@ -624,6 +647,8 @@ func Deserialize(commandType string, payload []byte) (Command, error) {
|
||||
cmd = &AdminDeleteRoom{}
|
||||
case MicUp{}.Type():
|
||||
cmd = &MicUp{}
|
||||
case RobotMicUp{}.Type():
|
||||
cmd = &RobotMicUp{}
|
||||
case MicDown{}.Type():
|
||||
cmd = &MicDown{}
|
||||
case ConfirmMicPublishing{}.Type():
|
||||
|
||||
566
services/room-service/internal/room/service/admin_robot_room.go
Normal file
566
services/room-service/internal/room/service/admin_robot_room.go
Normal file
@ -0,0 +1,566 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
"hyapp/pkg/appcode"
|
||||
"hyapp/pkg/logx"
|
||||
"hyapp/pkg/roomid"
|
||||
"hyapp/pkg/xerr"
|
||||
)
|
||||
|
||||
const (
|
||||
robotRoomStatusActive = "active"
|
||||
robotRoomStatusStopped = "stopped"
|
||||
robotRoomMode = "voice"
|
||||
robotRoomLuckyPoolID = "robot_lucky_display"
|
||||
)
|
||||
|
||||
func (s *Service) AdminListRobotRooms(ctx context.Context, req *roomv1.AdminListRobotRoomsRequest) (*roomv1.AdminListRobotRoomsResponse, error) {
|
||||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||
items, total, err := s.repository.ListRobotRooms(ctx, RobotRoomListQuery{
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
Status: strings.TrimSpace(req.GetStatus()),
|
||||
Page: int(req.GetPage()),
|
||||
PageSize: int(req.GetPageSize()),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &roomv1.AdminListRobotRoomsResponse{Total: total, ServerTimeMs: s.clock.Now().UnixMilli()}
|
||||
for _, item := range items {
|
||||
resp.Rooms = append(resp.Rooms, robotRoomConfigToProto(item))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) AdminFilterAvailableRoomRobots(ctx context.Context, req *roomv1.AdminFilterAvailableRoomRobotsRequest) (*roomv1.AdminFilterAvailableRoomRobotsResponse, error) {
|
||||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||
ids := normalizeRobotUserIDs(req.GetUserIds())
|
||||
occupied, err := s.repository.OccupiedRobotUserIDs(ctx, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &roomv1.AdminFilterAvailableRoomRobotsResponse{ServerTimeMs: s.clock.Now().UnixMilli()}
|
||||
for _, userID := range ids {
|
||||
if occupied[userID] {
|
||||
resp.OccupiedUserIds = append(resp.OccupiedUserIds, userID)
|
||||
continue
|
||||
}
|
||||
resp.AvailableUserIds = append(resp.AvailableUserIds, userID)
|
||||
}
|
||||
slices.Sort(resp.AvailableUserIds)
|
||||
slices.Sort(resp.OccupiedUserIds)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Service) AdminCreateRobotRoom(ctx context.Context, req *roomv1.AdminCreateRobotRoomRequest) (*roomv1.AdminCreateRobotRoomResponse, error) {
|
||||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||
now := s.clock.Now()
|
||||
config, err := s.buildRobotRoomConfig(ctx, req, now)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.bootstrapRobotRoom(ctx, config); err != nil {
|
||||
logx.Error(ctx, "admin_robot_room_bootstrap_failed", err,
|
||||
slog.String("room_id", config.RoomID),
|
||||
slog.Int64("owner_robot_user_id", config.OwnerRobotUserID),
|
||||
slog.Int("robot_count", len(config.RobotUserIDs)),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
saved, err := s.repository.CreateRobotRoomConfig(ctx, CreateRobotRoomConfigInput{Config: config, NowMS: now.UnixMilli()})
|
||||
if err != nil {
|
||||
logx.Error(ctx, "admin_robot_room_config_save_failed", err,
|
||||
slog.String("room_id", config.RoomID),
|
||||
slog.Int64("owner_robot_user_id", config.OwnerRobotUserID),
|
||||
slog.Int("robot_count", len(config.RobotUserIDs)),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
s.startRobotRoomRuntime(ctx, saved)
|
||||
return &roomv1.AdminCreateRobotRoomResponse{Room: robotRoomConfigToProto(saved), ServerTimeMs: now.UnixMilli()}, nil
|
||||
}
|
||||
|
||||
func (s *Service) AdminSetRobotRoomStatus(ctx context.Context, req *roomv1.AdminSetRobotRoomStatusRequest) (*roomv1.AdminSetRobotRoomStatusResponse, error) {
|
||||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||
roomID := strings.TrimSpace(req.GetMeta().GetRoomId())
|
||||
status := normalizeRobotRoomStatus(req.GetStatus())
|
||||
if roomID == "" {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "room_id is required")
|
||||
}
|
||||
if status == "" {
|
||||
return nil, xerr.New(xerr.InvalidArgument, "robot room status is invalid")
|
||||
}
|
||||
config, exists, err := s.repository.UpdateRobotRoomStatus(ctx, roomID, status, s.clock.Now().UnixMilli())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !exists {
|
||||
return nil, xerr.New(xerr.NotFound, "robot room not found")
|
||||
}
|
||||
if status == robotRoomStatusActive {
|
||||
s.startRobotRoomRuntime(ctx, config)
|
||||
} else {
|
||||
s.stopRobotRoomRuntime(roomID)
|
||||
}
|
||||
return &roomv1.AdminSetRobotRoomStatusResponse{Room: robotRoomConfigToProto(config), ServerTimeMs: s.clock.Now().UnixMilli()}, nil
|
||||
}
|
||||
|
||||
func (s *Service) buildRobotRoomConfig(ctx context.Context, req *roomv1.AdminCreateRobotRoomRequest, now time.Time) (RobotRoomConfig, error) {
|
||||
ownerID := req.GetOwnerRobotUserId()
|
||||
if ownerID <= 0 {
|
||||
return RobotRoomConfig{}, xerr.New(xerr.InvalidArgument, "owner_robot_user_id is required")
|
||||
}
|
||||
candidates := normalizeRobotUserIDs(req.GetCandidateRobotUserIds())
|
||||
containsOwner := false
|
||||
for _, userID := range candidates {
|
||||
if userID == ownerID {
|
||||
containsOwner = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !containsOwner {
|
||||
candidates = append(candidates, ownerID)
|
||||
}
|
||||
occupied, err := s.repository.OccupiedRobotUserIDs(ctx, candidates)
|
||||
if err != nil {
|
||||
return RobotRoomConfig{}, err
|
||||
}
|
||||
if occupied[ownerID] {
|
||||
return RobotRoomConfig{}, xerr.New(xerr.Conflict, "owner robot already assigned to active robot room")
|
||||
}
|
||||
available := make([]int64, 0, len(candidates))
|
||||
for _, userID := range candidates {
|
||||
if !occupied[userID] {
|
||||
available = append(available, userID)
|
||||
}
|
||||
}
|
||||
minCount, maxCount := normalizeRobotCountRange(req.GetMinRobotCount(), req.GetMaxRobotCount())
|
||||
if len(available) < minCount {
|
||||
return RobotRoomConfig{}, xerr.New(xerr.Conflict, "available robot count is not enough")
|
||||
}
|
||||
rng := rand.New(rand.NewSource(now.UnixNano()))
|
||||
selectedCount := randomIntRange(rng, minCount, maxCount)
|
||||
if selectedCount > len(available) {
|
||||
selectedCount = len(available)
|
||||
}
|
||||
selected := selectRobotUsers(rng, ownerID, available, selectedCount)
|
||||
rule, err := normalizeRobotRoomGiftRule(req.GetGiftRule())
|
||||
if err != nil {
|
||||
return RobotRoomConfig{}, err
|
||||
}
|
||||
roomID := fmt.Sprintf("robot_%d_%d", now.UnixMilli(), rng.Intn(900000)+100000)
|
||||
if !roomid.ValidStringID(roomID) {
|
||||
return RobotRoomConfig{}, xerr.New(xerr.Internal, "generated robot room id is invalid")
|
||||
}
|
||||
title := strings.TrimSpace(req.GetRoomName())
|
||||
if title == "" {
|
||||
title = fmt.Sprintf("Robot Room %d", ownerID)
|
||||
}
|
||||
coverURL := strings.TrimSpace(req.GetRoomAvatar())
|
||||
if coverURL == "" {
|
||||
coverURL = defaultRoomAvatar
|
||||
}
|
||||
return RobotRoomConfig{
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
RoomID: roomID,
|
||||
RoomShortID: fmt.Sprintf("%d", ownerID),
|
||||
Title: title,
|
||||
CoverURL: coverURL,
|
||||
VisibleRegionID: normalizeVisibleRegionID(req.GetVisibleRegionId()),
|
||||
Status: robotRoomStatusActive,
|
||||
OwnerRobotUserID: ownerID,
|
||||
RobotUserIDs: selected,
|
||||
GiftRule: rule,
|
||||
CreatedByAdminID: req.GetAdminId(),
|
||||
CreatedAtMS: now.UnixMilli(),
|
||||
UpdatedAtMS: now.UnixMilli(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) bootstrapRobotRoom(ctx context.Context, config RobotRoomConfig) error {
|
||||
_, err := s.CreateRoom(ctx, &roomv1.CreateRoomRequest{
|
||||
Meta: &roomv1.RequestMeta{
|
||||
RequestId: fmt.Sprintf("admin-robot-room-create-%s", config.RoomID),
|
||||
CommandId: fmt.Sprintf("admin-robot-room:%s:create", config.RoomID),
|
||||
ActorUserId: config.OwnerRobotUserID,
|
||||
RoomId: config.RoomID,
|
||||
AppCode: config.AppCode,
|
||||
SentAtMs: s.clock.Now().UnixMilli(),
|
||||
},
|
||||
SeatCount: int32(max(10, len(config.RobotUserIDs))),
|
||||
Mode: robotRoomMode,
|
||||
VisibleRegionId: config.VisibleRegionID,
|
||||
RoomName: config.Title,
|
||||
RoomAvatar: config.CoverURL,
|
||||
RoomShortId: config.RoomShortID,
|
||||
RobotRoom: true,
|
||||
RobotUserIds: config.RobotUserIDs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, userID := range config.RobotUserIDs {
|
||||
if userID == config.OwnerRobotUserID {
|
||||
continue
|
||||
}
|
||||
if _, err := s.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
||||
Meta: robotRoomCommandMeta(config, userID, fmt.Sprintf("join:%d", userID)),
|
||||
Role: "audience",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for index, userID := range config.RobotUserIDs {
|
||||
if _, err := s.RobotVirtualMicUp(ctx, RobotMicUpInput{
|
||||
Meta: robotRoomCommandMeta(config, config.OwnerRobotUserID, fmt.Sprintf("mic:%d", userID)),
|
||||
TargetUserID: userID,
|
||||
SeatNo: int32(index + 1),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunRobotRoomRuntimeManager 恢复 active 机器人房间的进程内送礼循环;配置事实仍以 MySQL 为准。
|
||||
func (s *Service) RunRobotRoomRuntimeManager(ctx context.Context, interval time.Duration) {
|
||||
if interval <= 0 {
|
||||
interval = 10 * time.Second
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
s.startActiveRobotRooms(ctx)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
s.stopAllRobotRoomRuntimes()
|
||||
return
|
||||
case <-ticker.C:
|
||||
s.startActiveRobotRooms(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) startActiveRobotRooms(ctx context.Context) {
|
||||
configs, err := s.repository.ListActiveRobotRooms(ctx)
|
||||
if err != nil {
|
||||
logx.Warn(ctx, "robot_room_runtime_scan_failed", slog.String("error", err.Error()))
|
||||
return
|
||||
}
|
||||
for _, config := range configs {
|
||||
s.startRobotRoomRuntime(ctx, config)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) startRobotRoomRuntime(parent context.Context, config RobotRoomConfig) {
|
||||
if config.Status != robotRoomStatusActive || len(config.RobotUserIDs) < 2 {
|
||||
return
|
||||
}
|
||||
s.robotRuntimeMu.Lock()
|
||||
if s.robotRuntimes == nil {
|
||||
s.robotRuntimes = make(map[string]context.CancelFunc)
|
||||
}
|
||||
if _, exists := s.robotRuntimes[config.RoomID]; exists {
|
||||
s.robotRuntimeMu.Unlock()
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(appcode.WithContext(parent, config.AppCode))
|
||||
s.robotRuntimes[config.RoomID] = cancel
|
||||
s.robotRuntimeMu.Unlock()
|
||||
go s.runRobotRoomRuntime(ctx, config)
|
||||
}
|
||||
|
||||
func (s *Service) stopRobotRoomRuntime(roomID string) {
|
||||
s.robotRuntimeMu.Lock()
|
||||
cancel := s.robotRuntimes[roomID]
|
||||
delete(s.robotRuntimes, roomID)
|
||||
s.robotRuntimeMu.Unlock()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) stopAllRobotRoomRuntimes() {
|
||||
s.robotRuntimeMu.Lock()
|
||||
cancels := make([]context.CancelFunc, 0, len(s.robotRuntimes))
|
||||
for roomID, cancel := range s.robotRuntimes {
|
||||
cancels = append(cancels, cancel)
|
||||
delete(s.robotRuntimes, roomID)
|
||||
}
|
||||
s.robotRuntimeMu.Unlock()
|
||||
for _, cancel := range cancels {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) runRobotRoomRuntime(ctx context.Context, config RobotRoomConfig) {
|
||||
for _, senderID := range config.RobotUserIDs {
|
||||
senderID := senderID
|
||||
go s.runRobotNormalGiftLoop(ctx, config, senderID)
|
||||
go s.runRobotLuckyGiftLoop(ctx, config, senderID)
|
||||
}
|
||||
<-ctx.Done()
|
||||
}
|
||||
|
||||
func (s *Service) runRobotNormalGiftLoop(ctx context.Context, config RobotRoomConfig, senderID int64) {
|
||||
interval := time.Duration(config.GiftRule.NormalGiftIntervalMS) * time.Millisecond
|
||||
if interval <= 0 || len(config.GiftRule.GiftIDs) == 0 {
|
||||
return
|
||||
}
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano() + senderID))
|
||||
timer := time.NewTimer(randomJitter(interval, rng))
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
targetID := randomRobotTarget(rng, config.RobotUserIDs, senderID)
|
||||
giftID := randomString(rng, config.GiftRule.GiftIDs)
|
||||
if targetID > 0 && giftID != "" {
|
||||
s.sendRobotGiftBestEffort(ctx, config, senderID, targetID, giftID, "", "normal")
|
||||
}
|
||||
timer.Reset(interval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) runRobotLuckyGiftLoop(ctx context.Context, config RobotRoomConfig, senderID int64) {
|
||||
if len(config.GiftRule.LuckyGiftIDs) == 0 || config.GiftRule.LuckyComboMax <= 0 {
|
||||
return
|
||||
}
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano() + senderID*17))
|
||||
for {
|
||||
combo := randomInt64Range(rng, config.GiftRule.LuckyComboMin, config.GiftRule.LuckyComboMax)
|
||||
for i := int64(0); i < combo; i++ {
|
||||
giftID := randomString(rng, config.GiftRule.LuckyGiftIDs)
|
||||
for _, targetID := range config.RobotUserIDs {
|
||||
if targetID == senderID {
|
||||
continue
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return
|
||||
}
|
||||
s.sendRobotGiftBestEffort(ctx, config, senderID, targetID, giftID, robotRoomLuckyPoolID, "lucky")
|
||||
}
|
||||
}
|
||||
pause := time.Duration(randomInt64Range(rng, config.GiftRule.LuckyPauseMinMS, config.GiftRule.LuckyPauseMaxMS)) * time.Millisecond
|
||||
if pause <= 0 {
|
||||
pause = 5 * time.Second
|
||||
}
|
||||
timer := time.NewTimer(pause)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) sendRobotGiftBestEffort(ctx context.Context, config RobotRoomConfig, senderID int64, targetID int64, giftID string, poolID string, kind string) {
|
||||
_, err := s.RobotSendGift(ctx, RobotSendGiftInput{
|
||||
Meta: robotRoomCommandMeta(config, senderID, fmt.Sprintf("gift:%s:%d:%d", kind, targetID, time.Now().UTC().UnixNano())),
|
||||
TargetUserID: targetID,
|
||||
GiftID: giftID,
|
||||
GiftCount: 1,
|
||||
PoolID: poolID,
|
||||
RobotUserIDs: config.RobotUserIDs,
|
||||
SyntheticRewardCoins: 0,
|
||||
SyntheticMultiplierPPM: 0,
|
||||
})
|
||||
if err != nil {
|
||||
logx.Warn(ctx, "robot_room_send_gift_failed", slog.String("room_id", config.RoomID), slog.String("kind", kind), slog.Int64("sender_user_id", senderID), slog.Int64("target_user_id", targetID), slog.String("gift_id", giftID), slog.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func robotRoomCommandMeta(config RobotRoomConfig, actorUserID int64, suffix string) *roomv1.RequestMeta {
|
||||
commandID := fmt.Sprintf("robot-room:%s:%s", config.RoomID, strings.TrimSpace(suffix))
|
||||
if len(commandID) > 128 {
|
||||
commandID = commandID[:128]
|
||||
}
|
||||
return &roomv1.RequestMeta{
|
||||
RequestId: commandID,
|
||||
CommandId: commandID,
|
||||
ActorUserId: actorUserID,
|
||||
RoomId: config.RoomID,
|
||||
AppCode: config.AppCode,
|
||||
SentAtMs: time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRobotRoomGiftRule(input *roomv1.AdminRobotRoomGiftRule) (RobotRoomGiftRule, error) {
|
||||
if input == nil {
|
||||
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "gift_rule is required")
|
||||
}
|
||||
rule := RobotRoomGiftRule{
|
||||
GiftIDs: normalizeStringSet(input.GetGiftIds()),
|
||||
LuckyGiftIDs: normalizeStringSet(input.GetLuckyGiftIds()),
|
||||
NormalGiftIntervalMS: input.GetNormalGiftIntervalMs(),
|
||||
LuckyComboMin: input.GetLuckyComboMin(),
|
||||
LuckyComboMax: input.GetLuckyComboMax(),
|
||||
LuckyPauseMinMS: input.GetLuckyPauseMinMs(),
|
||||
LuckyPauseMaxMS: input.GetLuckyPauseMaxMs(),
|
||||
}
|
||||
if len(rule.GiftIDs) == 0 {
|
||||
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "gift_ids is required")
|
||||
}
|
||||
if len(rule.LuckyGiftIDs) == 0 {
|
||||
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "lucky_gift_ids is required")
|
||||
}
|
||||
if rule.NormalGiftIntervalMS <= 0 {
|
||||
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "normal_gift_interval_ms is required")
|
||||
}
|
||||
if rule.LuckyComboMin <= 0 || rule.LuckyComboMax < rule.LuckyComboMin {
|
||||
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "lucky combo range is invalid")
|
||||
}
|
||||
if rule.LuckyPauseMinMS <= 0 || rule.LuckyPauseMaxMS < rule.LuckyPauseMinMS {
|
||||
return RobotRoomGiftRule{}, xerr.New(xerr.InvalidArgument, "lucky pause range is invalid")
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func robotRoomConfigToProto(config RobotRoomConfig) *roomv1.AdminRobotRoom {
|
||||
return &roomv1.AdminRobotRoom{
|
||||
AppCode: config.AppCode,
|
||||
RoomId: config.RoomID,
|
||||
RoomShortId: config.RoomShortID,
|
||||
Title: config.Title,
|
||||
CoverUrl: config.CoverURL,
|
||||
VisibleRegionId: config.VisibleRegionID,
|
||||
Status: config.Status,
|
||||
OwnerRobotUserId: config.OwnerRobotUserID,
|
||||
RobotUserIds: append([]int64(nil), config.RobotUserIDs...),
|
||||
GiftRule: &roomv1.AdminRobotRoomGiftRule{
|
||||
GiftIds: append([]string(nil), config.GiftRule.GiftIDs...),
|
||||
LuckyGiftIds: append([]string(nil), config.GiftRule.LuckyGiftIDs...),
|
||||
NormalGiftIntervalMs: config.GiftRule.NormalGiftIntervalMS,
|
||||
LuckyComboMin: config.GiftRule.LuckyComboMin,
|
||||
LuckyComboMax: config.GiftRule.LuckyComboMax,
|
||||
LuckyPauseMinMs: config.GiftRule.LuckyPauseMinMS,
|
||||
LuckyPauseMaxMs: config.GiftRule.LuckyPauseMaxMS,
|
||||
},
|
||||
CreatedByAdminId: config.CreatedByAdminID,
|
||||
CreatedAtMs: config.CreatedAtMS,
|
||||
UpdatedAtMs: config.UpdatedAtMS,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRobotCountRange(minCount int32, maxCount int32) (int, int) {
|
||||
minValue := int(minCount)
|
||||
maxValue := int(maxCount)
|
||||
if minValue <= 0 {
|
||||
minValue = 6
|
||||
}
|
||||
if maxValue <= 0 {
|
||||
maxValue = 8
|
||||
}
|
||||
if maxValue < minValue {
|
||||
maxValue = minValue
|
||||
}
|
||||
return minValue, maxValue
|
||||
}
|
||||
|
||||
func selectRobotUsers(rng *rand.Rand, ownerID int64, available []int64, count int) []int64 {
|
||||
others := make([]int64, 0, len(available))
|
||||
for _, userID := range available {
|
||||
if userID != ownerID {
|
||||
others = append(others, userID)
|
||||
}
|
||||
}
|
||||
rng.Shuffle(len(others), func(i, j int) { others[i], others[j] = others[j], others[i] })
|
||||
selected := []int64{ownerID}
|
||||
for _, userID := range others {
|
||||
if len(selected) >= count {
|
||||
break
|
||||
}
|
||||
selected = append(selected, userID)
|
||||
}
|
||||
slices.Sort(selected)
|
||||
return selected
|
||||
}
|
||||
|
||||
func normalizeRobotRoomStatus(status string) string {
|
||||
switch strings.TrimSpace(status) {
|
||||
case robotRoomStatusActive:
|
||||
return robotRoomStatusActive
|
||||
case robotRoomStatusStopped:
|
||||
return robotRoomStatusStopped
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeStringSet(values []string) []string {
|
||||
seen := make(map[string]bool, len(values))
|
||||
out := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" || seen[value] {
|
||||
continue
|
||||
}
|
||||
seen[value] = true
|
||||
out = append(out, value)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func int64SetKeys(values map[int64]bool) []int64 {
|
||||
out := make([]int64, 0, len(values))
|
||||
for value := range values {
|
||||
out = append(out, value)
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func randomString(rng *rand.Rand, values []string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return values[rng.Intn(len(values))]
|
||||
}
|
||||
|
||||
func randomRobotTarget(rng *rand.Rand, robots []int64, senderID int64) int64 {
|
||||
targets := make([]int64, 0, len(robots))
|
||||
for _, userID := range robots {
|
||||
if userID != senderID {
|
||||
targets = append(targets, userID)
|
||||
}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return 0
|
||||
}
|
||||
return targets[rng.Intn(len(targets))]
|
||||
}
|
||||
|
||||
func randomIntRange(rng *rand.Rand, minValue int, maxValue int) int {
|
||||
if maxValue <= minValue {
|
||||
return minValue
|
||||
}
|
||||
return minValue + rng.Intn(maxValue-minValue+1)
|
||||
}
|
||||
|
||||
func randomInt64Range(rng *rand.Rand, minValue int64, maxValue int64) int64 {
|
||||
if maxValue <= minValue {
|
||||
return minValue
|
||||
}
|
||||
return minValue + rng.Int63n(maxValue-minValue+1)
|
||||
}
|
||||
|
||||
func randomJitter(interval time.Duration, rng *rand.Rand) time.Duration {
|
||||
if interval <= time.Second {
|
||||
return interval
|
||||
}
|
||||
return time.Duration(rng.Int63n(int64(interval))) + time.Second
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@ -27,6 +28,8 @@ const (
|
||||
roomExtShortIDKey = "room_short_id"
|
||||
roomExtOwnerCountryKey = "owner_country_code"
|
||||
roomExtBackgroundKey = "background_url"
|
||||
roomExtRobotRoomKey = "robot_room"
|
||||
roomExtRobotUserIDsKey = "robot_user_ids"
|
||||
|
||||
// defaultRoomAvatar 只服务房间资料更新/后台清空封面后的兜底;创建房间必须显式提交封面。
|
||||
defaultRoomAvatar = "hyapp://room/default-avatar"
|
||||
@ -104,6 +107,8 @@ func (s *Service) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest)
|
||||
RoomAvatar: profile.Avatar,
|
||||
RoomDescription: profile.Description,
|
||||
RoomShortID: normalizeRoomShortID(req.GetRoomShortId(), actorUserID),
|
||||
RobotRoom: req.GetRobotRoom(),
|
||||
RobotUserIDs: normalizeRobotUserIDs(req.GetRobotUserIds()),
|
||||
}
|
||||
|
||||
idempotencyStartedAt := time.Now()
|
||||
@ -183,6 +188,7 @@ func (s *Service) CreateRoom(ctx context.Context, req *roomv1.CreateRoomRequest)
|
||||
ShortID: cmd.RoomShortID,
|
||||
OwnerCountryCode: cmd.OwnerCountryCode,
|
||||
})
|
||||
applyRobotRoomExt(roomState, cmd.RobotRoom, cmd.RobotUserIDs)
|
||||
roomState.OnlineUsers[cmd.ActorUserID()] = &state.RoomUserState{
|
||||
UserID: cmd.ActorUserID(),
|
||||
Role: "owner",
|
||||
@ -338,3 +344,46 @@ func applyRoomProfileExt(roomState *state.RoomState, profile roomProfileInput) {
|
||||
roomState.RoomExt[roomExtShortIDKey] = profile.ShortID
|
||||
roomState.RoomExt[roomExtOwnerCountryKey] = normalizeRoomCountryCode(profile.OwnerCountryCode)
|
||||
}
|
||||
|
||||
func applyRobotRoomExt(roomState *state.RoomState, robotRoom bool, robotUserIDs []int64) {
|
||||
if roomState == nil || !robotRoom {
|
||||
return
|
||||
}
|
||||
if roomState.RoomExt == nil {
|
||||
roomState.RoomExt = make(map[string]string)
|
||||
}
|
||||
roomState.RoomExt[roomExtRobotRoomKey] = "true"
|
||||
roomState.RoomExt[roomExtRobotUserIDsKey] = joinInt64CSV(normalizeRobotUserIDs(robotUserIDs))
|
||||
}
|
||||
|
||||
func normalizeRobotUserIDs(values []int64) []int64 {
|
||||
seen := make(map[int64]bool, len(values))
|
||||
out := make([]int64, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value <= 0 || seen[value] {
|
||||
continue
|
||||
}
|
||||
seen[value] = true
|
||||
out = append(out, value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func joinInt64CSV(values []int64) string {
|
||||
parts := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
parts = append(parts, strconv.FormatInt(value, 10))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func parseInt64CSV(raw string) map[int64]bool {
|
||||
result := make(map[int64]bool)
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
value, err := strconv.ParseInt(strings.TrimSpace(part), 10, 64)
|
||||
if err == nil && value > 0 {
|
||||
result[value] = true
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -24,6 +24,10 @@ func (followTestWallet) BatchDebitGift(context.Context, *walletv1.BatchDebitGift
|
||||
return &walletv1.BatchDebitGiftResponse{Aggregate: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-follow-test"}}, nil
|
||||
}
|
||||
|
||||
func (followTestWallet) DebitRobotGift(context.Context, *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error) {
|
||||
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-robot-follow-test"}, nil
|
||||
}
|
||||
|
||||
func (followTestWallet) GrantResourceGroup(context.Context, *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
|
||||
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "grant-follow-test"}}, nil
|
||||
}
|
||||
|
||||
@ -23,6 +23,46 @@ import (
|
||||
|
||||
// SendGift 先同步调用 wallet,再在 Room Cell 内完成热度、榜单和房间事件落地。
|
||||
func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*roomv1.SendGiftResponse, error) {
|
||||
return s.sendGift(ctx, req, robotGiftOptions{})
|
||||
}
|
||||
|
||||
type RobotSendGiftInput struct {
|
||||
Meta *roomv1.RequestMeta
|
||||
TargetUserID int64
|
||||
GiftID string
|
||||
GiftCount int32
|
||||
PoolID string
|
||||
RobotUserIDs []int64
|
||||
SyntheticRewardCoins int64
|
||||
SyntheticMultiplierPPM int64
|
||||
}
|
||||
|
||||
type robotGiftOptions struct {
|
||||
Enabled bool
|
||||
RobotUserIDs []int64
|
||||
SyntheticRewardCoins int64
|
||||
SyntheticMultiplierPPM int64
|
||||
}
|
||||
|
||||
func (s *Service) RobotSendGift(ctx context.Context, input RobotSendGiftInput) (*roomv1.SendGiftResponse, error) {
|
||||
req := &roomv1.SendGiftRequest{
|
||||
Meta: input.Meta,
|
||||
TargetUserId: input.TargetUserID,
|
||||
TargetUserIds: []int64{input.TargetUserID},
|
||||
GiftId: input.GiftID,
|
||||
GiftCount: input.GiftCount,
|
||||
TargetType: "user",
|
||||
PoolId: input.PoolID,
|
||||
}
|
||||
return s.sendGift(ctx, req, robotGiftOptions{
|
||||
Enabled: true,
|
||||
RobotUserIDs: input.RobotUserIDs,
|
||||
SyntheticRewardCoins: input.SyntheticRewardCoins,
|
||||
SyntheticMultiplierPPM: input.SyntheticMultiplierPPM,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) sendGift(ctx context.Context, req *roomv1.SendGiftRequest, robotOptions robotGiftOptions) (*roomv1.SendGiftResponse, error) {
|
||||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||||
// SendGift 的账务不在 room-service 内结算,但房间表现、热度和榜单由 Room Cell 同步更新。
|
||||
cmd := command.SendGift{
|
||||
@ -38,6 +78,10 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
// 工资区域由 gateway 从 user-service host profile 注入;房间 visible_region_id 只用于房间/礼物可见性。
|
||||
TargetHostRegionID: req.GetTargetHostRegionId(),
|
||||
TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(),
|
||||
RobotGift: robotOptions.Enabled,
|
||||
SyntheticLuckyGift: robotOptions.Enabled && strings.TrimSpace(req.GetPoolId()) != "",
|
||||
SyntheticLuckyRewardCoins: firstPositiveInt64(robotOptions.SyntheticRewardCoins, 0),
|
||||
SyntheticLuckyMultiplierPPM: firstPositiveInt64(robotOptions.SyntheticMultiplierPPM, 1000000),
|
||||
}
|
||||
if cmd.TargetType == "" {
|
||||
cmd.TargetType = "user"
|
||||
@ -65,6 +109,11 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not in room")
|
||||
}
|
||||
}
|
||||
if cmd.RobotGift {
|
||||
if err := requireRobotGiftParticipants(current, cmd.ActorUserID(), cmd.TargetUserIDs, robotOptions.RobotUserIDs); err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.GiftID == "" || cmd.GiftCount <= 0 {
|
||||
// 礼物 ID 和数量是钱包查服务端价格的最小输入,客户端不再提交礼物单价。
|
||||
@ -78,7 +127,7 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "room not found")
|
||||
}
|
||||
luckyEnabled := false
|
||||
if cmd.PoolID != "" {
|
||||
if cmd.PoolID != "" && !cmd.SyntheticLuckyGift {
|
||||
// 客户端显式传 pool_id 时必须先检查活动规则;规则未启用就拒绝,避免先扣费再发现不能抽奖。
|
||||
if s.luckyGift == nil {
|
||||
return mutationResult{}, nil, xerr.New(xerr.Unavailable, "lucky gift service is not configured")
|
||||
@ -129,11 +178,16 @@ 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 cmd.SyntheticLuckyGift {
|
||||
luckyGift = syntheticLuckyGiftResult(cmd, targetBillings, now)
|
||||
if luckyGift != nil {
|
||||
luckyGifts = []*roomv1.LuckyGiftDrawResult{luckyGift}
|
||||
}
|
||||
} else if !luckyEnabled {
|
||||
// 未显式传 pool_id 的旧客户端仍可通过钱包礼物类型触发默认幸运礼物逻辑;新客户端应传明确 pool_id。
|
||||
luckyEnabled = s.shouldDrawLuckyGift(cmd.PoolID, billing.GetGiftTypeCode())
|
||||
}
|
||||
if luckyEnabled {
|
||||
if !cmd.SyntheticLuckyGift && luckyEnabled {
|
||||
luckyGifts, err = s.executeLuckyGiftDraws(ctx, req.GetMeta(), cmd, roomMeta, targetBillings, now)
|
||||
if err != nil {
|
||||
// 抽奖失败时整笔送礼失败并依赖 wallet 幂等重试;不能落普通礼物后丢失幸运礼物抽奖事实。
|
||||
@ -207,6 +261,8 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
TargetGiftValue: targetCurrentGiftValues[targetBilling.TargetUserID],
|
||||
// effect_types 来自后台礼物配置,房间事件必须原样带出,后续 activity-service 才能识别全服广播标识。
|
||||
GiftEffectTypes: targetBilling.Billing.GetGiftEffectTypes(),
|
||||
IsRobotGift: cmd.RobotGift,
|
||||
SyntheticLuckyGift: cmd.SyntheticLuckyGift,
|
||||
})
|
||||
if err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
@ -249,8 +305,32 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
}
|
||||
records = append(records, ignitedEvent)
|
||||
}
|
||||
if cmd.SyntheticLuckyGift && luckyGift != nil {
|
||||
drawEvent, err := outbox.Build(current.RoomID, "RoomRobotLuckyGiftDrawn", current.Version, now, &roomeventsv1.RoomRobotLuckyGiftDrawn{
|
||||
DrawId: luckyGift.GetDrawId(),
|
||||
CommandId: cmd.ID(),
|
||||
SenderUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
GiftId: cmd.GiftID,
|
||||
GiftCount: cmd.GiftCount,
|
||||
PoolId: cmd.PoolID,
|
||||
MultiplierPpm: luckyGift.GetMultiplierPpm(),
|
||||
BaseRewardCoins: luckyGift.GetBaseRewardCoins(),
|
||||
RoomAtmosphereRewardCoins: luckyGift.GetRoomAtmosphereRewardCoins(),
|
||||
ActivitySubsidyCoins: luckyGift.GetActivitySubsidyCoins(),
|
||||
EffectiveRewardCoins: luckyGift.GetEffectiveRewardCoins(),
|
||||
CreatedAtMs: luckyGift.GetCreatedAtMs(),
|
||||
VisibleRegionId: roomMeta.VisibleRegionID,
|
||||
IsRobot: true,
|
||||
Synthetic: true,
|
||||
})
|
||||
if err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
records = append(records, drawEvent)
|
||||
}
|
||||
|
||||
return mutationResult{
|
||||
result := mutationResult{
|
||||
snapshot: current.ToProto(),
|
||||
billingReceiptID: billing.GetBillingReceiptId(),
|
||||
roomHeat: current.Heat,
|
||||
@ -301,9 +381,17 @@ func (s *Service) SendGift(ctx context.Context, req *roomv1.SendGiftRequest) (*r
|
||||
"gift_icon_url": billing.GetGiftIconUrl(),
|
||||
"gift_animation_url": billing.GetGiftAnimationUrl(),
|
||||
"target_gift_value": fmt.Sprintf("%d", targetCurrentGiftValues[cmd.TargetUserID]),
|
||||
"is_robot_gift": fmt.Sprintf("%t", cmd.RobotGift),
|
||||
"synthetic_lucky_gift": fmt.Sprintf("%t", cmd.SyntheticLuckyGift),
|
||||
},
|
||||
},
|
||||
}, records, nil
|
||||
}
|
||||
if cmd.RobotGift {
|
||||
// 机器人房间可以增加房间热度和麦位收礼热度,但不进入跨房礼物榜和房间送礼统计。
|
||||
result.roomGiftLeaderboard = nil
|
||||
result.roomUserGiftStats = nil
|
||||
}
|
||||
return result, records, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -328,6 +416,26 @@ type giftTargetBilling struct {
|
||||
}
|
||||
|
||||
func (s *Service) debitGiftTargets(ctx context.Context, cmd command.SendGift, roomMeta RoomMeta) (*walletv1.DebitGiftResponse, []giftTargetBilling, error) {
|
||||
if cmd.RobotGift {
|
||||
if len(cmd.TargetUserIDs) != 1 {
|
||||
return nil, nil, xerr.New(xerr.InvalidArgument, "robot gift only supports one target")
|
||||
}
|
||||
billing, err := s.wallet.DebitRobotGift(ctx, &walletv1.DebitRobotGiftRequest{
|
||||
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,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return billing, []giftTargetBilling{{TargetUserID: cmd.TargetUserID, CommandID: cmd.ID(), Billing: billing}}, nil
|
||||
}
|
||||
if len(cmd.TargetUserIDs) == 1 {
|
||||
// 单目标保留原 wallet command_id,避免改变已有幂等键、账务回执和排障路径。
|
||||
billing, err := s.wallet.DebitGift(ctx, &walletv1.DebitGiftRequest{
|
||||
@ -657,3 +765,56 @@ func findRankItem(items []state.RankItem, userID int64) state.RankItem {
|
||||
|
||||
return state.RankItem{UserID: userID}
|
||||
}
|
||||
|
||||
func requireRobotGiftParticipants(current *state.RoomState, senderUserID int64, targetUserIDs []int64, runtimeRobotUserIDs []int64) error {
|
||||
if current == nil || current.RoomExt[roomExtRobotRoomKey] != "true" {
|
||||
return xerr.New(xerr.PermissionDenied, "robot gift requires robot room")
|
||||
}
|
||||
allowed := parseInt64CSV(current.RoomExt[roomExtRobotUserIDsKey])
|
||||
for _, userID := range runtimeRobotUserIDs {
|
||||
if userID > 0 {
|
||||
allowed[userID] = true
|
||||
}
|
||||
}
|
||||
if !allowed[senderUserID] {
|
||||
return xerr.New(xerr.PermissionDenied, "robot sender is not allowed")
|
||||
}
|
||||
for _, targetUserID := range targetUserIDs {
|
||||
if !allowed[targetUserID] {
|
||||
return xerr.New(xerr.PermissionDenied, "robot gift target is not allowed")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func syntheticLuckyGiftResult(cmd command.SendGift, targetBillings []giftTargetBilling, now time.Time) *roomv1.LuckyGiftDrawResult {
|
||||
if len(targetBillings) == 0 || targetBillings[0].Billing == nil {
|
||||
return nil
|
||||
}
|
||||
rewardCoins := firstPositiveInt64(cmd.SyntheticLuckyRewardCoins, targetBillings[0].Billing.GetCoinSpent()*5)
|
||||
multiplier := firstPositiveInt64(cmd.SyntheticLuckyMultiplierPPM, 5000000)
|
||||
return &roomv1.LuckyGiftDrawResult{
|
||||
Enabled: true,
|
||||
DrawId: fmt.Sprintf("robot_lucky_%s_%d", cmd.ID(), now.UnixMilli()),
|
||||
CommandId: cmd.ID(),
|
||||
PoolId: cmd.PoolID,
|
||||
GiftId: cmd.GiftID,
|
||||
MultiplierPpm: multiplier,
|
||||
BaseRewardCoins: rewardCoins,
|
||||
EffectiveRewardCoins: rewardCoins,
|
||||
RewardStatus: "granted",
|
||||
StageFeedback: true,
|
||||
HighMultiplier: multiplier >= 5000000,
|
||||
CreatedAtMs: now.UnixMilli(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
}
|
||||
}
|
||||
|
||||
func firstPositiveInt64(values ...int64) int64 {
|
||||
for _, value := range values {
|
||||
if value > 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@ -130,6 +130,7 @@ func replay(current *state.RoomState, cmd command.Command, committedAtMS int64)
|
||||
Description: typed.RoomDescription,
|
||||
ShortID: typed.RoomShortID,
|
||||
})
|
||||
applyRobotRoomExt(current, typed.RobotRoom, typed.RobotUserIDs)
|
||||
// 创建命令的 owner/mode 等基础 meta 已由 RoomMeta 或快照恢复,这里只保证版本进入初始提交态。
|
||||
current.Version = 1
|
||||
case *command.UpdateRoomProfile:
|
||||
@ -212,6 +213,20 @@ func replay(current *state.RoomState, cmd command.Command, committedAtMS int64)
|
||||
current.MicSeats[index].MicSessionRoomVersion = current.Version
|
||||
// pending_publish 还没有接受 RTC/客户端事件,恢复时保持 0,避免客户端时间慢于服务端命令时间时无法确认。
|
||||
current.MicSeats[index].LastPublishEventTimeMS = 0
|
||||
case *command.RobotMicUp:
|
||||
index := current.SeatIndex(typed.SeatNo)
|
||||
if index < 0 {
|
||||
return fmt.Errorf("robot_mic_up replay references missing seat: %d", typed.SeatNo)
|
||||
}
|
||||
current.Version++
|
||||
current.MicSeats[index].UserID = typed.TargetUserID
|
||||
current.MicSeats[index].PublishState = state.MicPublishPublishing
|
||||
current.MicSeats[index].MicSessionID = typed.MicSessionID
|
||||
current.MicSeats[index].PublishDeadlineMS = 0
|
||||
current.MicSeats[index].MicSessionRoomVersion = current.Version
|
||||
current.MicSeats[index].LastPublishEventTimeMS = typed.SentAtMS
|
||||
current.MicSeats[index].MicHeartbeatAtMS = typed.SentAtMS
|
||||
current.MicSeats[index].MicMuted = true
|
||||
case *command.MicDown:
|
||||
seat, exists := current.SeatByUser(typed.TargetUserID)
|
||||
if !exists {
|
||||
|
||||
@ -309,6 +309,48 @@ type GiftFuelRuleConfig struct {
|
||||
Excluded bool `json:"excluded"`
|
||||
}
|
||||
|
||||
// RobotRoomGiftRule 是后台配置的机器人房间送礼节奏;它只驱动机器人展示链路,不进入真实活动奖池。
|
||||
type RobotRoomGiftRule struct {
|
||||
GiftIDs []string
|
||||
LuckyGiftIDs []string
|
||||
NormalGiftIntervalMS int64
|
||||
LuckyComboMin int64
|
||||
LuckyComboMax int64
|
||||
LuckyPauseMinMS int64
|
||||
LuckyPauseMaxMS int64
|
||||
}
|
||||
|
||||
// RobotRoomConfig 是 room-service 持久化的机器人房间配置和账号占用事实。
|
||||
type RobotRoomConfig struct {
|
||||
AppCode string
|
||||
RoomID string
|
||||
RoomShortID string
|
||||
Title string
|
||||
CoverURL string
|
||||
VisibleRegionID int64
|
||||
Status string
|
||||
OwnerRobotUserID int64
|
||||
RobotUserIDs []int64
|
||||
GiftRule RobotRoomGiftRule
|
||||
CreatedByAdminID uint64
|
||||
CreatedAtMS int64
|
||||
UpdatedAtMS int64
|
||||
}
|
||||
|
||||
// RobotRoomListQuery 是后台机器人房间列表查询条件。
|
||||
type RobotRoomListQuery struct {
|
||||
AppCode string
|
||||
Status string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
// CreateRobotRoomConfigInput 在同一事务中锁定机器人账号并写入房间配置。
|
||||
type CreateRobotRoomConfigInput struct {
|
||||
Config RobotRoomConfig
|
||||
NowMS int64
|
||||
}
|
||||
|
||||
// RoomPresence 是当前用户可恢复房间的轻量读模型。
|
||||
// 它从 RoomSnapshot 投影而来,只用于 App 启动/回前台探测,不替代 Room Cell 权威状态。
|
||||
type RoomPresence struct {
|
||||
@ -577,6 +619,12 @@ type Repository interface {
|
||||
AdminListRoomPins(ctx context.Context, query AdminRoomPinQuery) ([]AdminRoomPinEntry, int64, error)
|
||||
AdminCreateRoomPin(ctx context.Context, input AdminCreateRoomPinInput) (AdminRoomPinEntry, bool, error)
|
||||
AdminCancelRoomPin(ctx context.Context, pinID int64, adminID uint64, nowMS int64) (AdminRoomPinEntry, bool, error)
|
||||
ListRobotRooms(ctx context.Context, query RobotRoomListQuery) ([]RobotRoomConfig, int64, error)
|
||||
GetRobotRoom(ctx context.Context, roomID string) (RobotRoomConfig, bool, error)
|
||||
CreateRobotRoomConfig(ctx context.Context, input CreateRobotRoomConfigInput) (RobotRoomConfig, error)
|
||||
UpdateRobotRoomStatus(ctx context.Context, roomID string, status string, nowMS int64) (RobotRoomConfig, bool, error)
|
||||
OccupiedRobotUserIDs(ctx context.Context, userIDs []int64) (map[int64]bool, error)
|
||||
ListActiveRobotRooms(ctx context.Context) ([]RobotRoomConfig, error)
|
||||
}
|
||||
|
||||
// RoomPresenceSnapshot 是 repository 投影接口的稳定入参,避免存储层反向依赖 protobuf。
|
||||
|
||||
126
services/room-service/internal/room/service/robot_mic.go
Normal file
126
services/room-service/internal/room/service/robot_mic.go
Normal file
@ -0,0 +1,126 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||||
roomv1 "hyapp.local/api/proto/room/v1"
|
||||
"hyapp/pkg/tencentim"
|
||||
"hyapp/pkg/xerr"
|
||||
"hyapp/services/room-service/internal/room/command"
|
||||
"hyapp/services/room-service/internal/room/outbox"
|
||||
"hyapp/services/room-service/internal/room/rank"
|
||||
"hyapp/services/room-service/internal/room/state"
|
||||
)
|
||||
|
||||
// RobotMicUpInput 是内部机器人编排器占麦输入;调用方必须只传 robot-service 登记过的机器人用户。
|
||||
type RobotMicUpInput struct {
|
||||
Meta *roomv1.RequestMeta
|
||||
TargetUserID int64
|
||||
SeatNo int32
|
||||
}
|
||||
|
||||
// RobotVirtualMicUp 把机器人放到静音虚拟麦位,不要求真实 RTC 推流。
|
||||
func (s *Service) RobotVirtualMicUp(ctx context.Context, input RobotMicUpInput) (*roomv1.MicUpResponse, error) {
|
||||
ctx = contextFromMeta(ctx, input.Meta)
|
||||
cmd := command.RobotMicUp{
|
||||
Base: baseFromMeta(input.Meta),
|
||||
TargetUserID: input.TargetUserID,
|
||||
SeatNo: input.SeatNo,
|
||||
}
|
||||
if cmd.TargetUserID <= 0 {
|
||||
cmd.TargetUserID = cmd.ActorUserID()
|
||||
}
|
||||
cmd.MicSessionID = micSessionIDForCommand(cmd.RoomID(), cmd.ID())
|
||||
|
||||
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||||
if err := requireActiveRoom(current); err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
if _, exists := current.OnlineUsers[cmd.TargetUserID]; !exists {
|
||||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "robot not in room")
|
||||
}
|
||||
if _, exists := current.SeatByUser(cmd.TargetUserID); exists {
|
||||
return mutationResult{}, nil, xerr.New(xerr.Conflict, "robot already on seat")
|
||||
}
|
||||
index := current.SeatIndex(cmd.SeatNo)
|
||||
if index < 0 {
|
||||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "seat does not exist")
|
||||
}
|
||||
if current.MicSeats[index].Locked {
|
||||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "seat is locked")
|
||||
}
|
||||
if current.MicSeats[index].UserID != 0 {
|
||||
return mutationResult{}, nil, xerr.New(xerr.Conflict, "seat is occupied")
|
||||
}
|
||||
|
||||
current.Version++
|
||||
current.MicSeats[index].UserID = cmd.TargetUserID
|
||||
current.MicSeats[index].PublishState = state.MicPublishPublishing
|
||||
current.MicSeats[index].MicSessionID = cmd.MicSessionID
|
||||
current.MicSeats[index].PublishDeadlineMS = 0
|
||||
current.MicSeats[index].MicSessionRoomVersion = current.Version
|
||||
current.MicSeats[index].LastPublishEventTimeMS = now.UnixMilli()
|
||||
current.MicSeats[index].MicHeartbeatAtMS = now.UnixMilli()
|
||||
current.MicSeats[index].MicMuted = true
|
||||
seatStatus := current.MicSeats[index].SeatStatus()
|
||||
targetGiftValue := roomUserGiftValue(current, cmd.TargetUserID)
|
||||
|
||||
micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, &roomeventsv1.RoomMicChanged{
|
||||
ActorUserId: cmd.ActorUserID(),
|
||||
TargetUserId: cmd.TargetUserID,
|
||||
FromSeat: 0,
|
||||
ToSeat: cmd.SeatNo,
|
||||
Action: "robot_up",
|
||||
MicSessionId: cmd.MicSessionID,
|
||||
PublishState: state.MicPublishPublishing,
|
||||
PublishDeadlineMs: 0,
|
||||
SeatStatus: seatStatus,
|
||||
TargetGiftValue: targetGiftValue,
|
||||
})
|
||||
if err != nil {
|
||||
return mutationResult{}, nil, err
|
||||
}
|
||||
|
||||
return mutationResult{
|
||||
snapshot: current.ToProto(),
|
||||
seatNo: cmd.SeatNo,
|
||||
micSessionID: cmd.MicSessionID,
|
||||
publishDeadlineMS: 0,
|
||||
syncEvent: &tencentim.RoomEvent{
|
||||
EventID: micEvent.EventID,
|
||||
RoomID: current.RoomID,
|
||||
EventType: "room_mic_publish_confirmed",
|
||||
ActorUserID: cmd.ActorUserID(),
|
||||
TargetUserID: cmd.TargetUserID,
|
||||
SeatNo: cmd.SeatNo,
|
||||
RoomVersion: current.Version,
|
||||
Attributes: map[string]string{
|
||||
"action": "robot_up",
|
||||
"publish_state": state.MicPublishPublishing,
|
||||
"mic_session_id": cmd.MicSessionID,
|
||||
"seat_status": seatStatus,
|
||||
"mic_muted": "true",
|
||||
"target_gift_value": int64String(targetGiftValue),
|
||||
},
|
||||
},
|
||||
}, []outbox.Record{micEvent}, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if result.micSessionID == "" {
|
||||
if seat := protoSeatByUser(result.snapshot, cmd.TargetUserID); seat != nil {
|
||||
result.seatNo = seat.GetSeatNo()
|
||||
result.micSessionID = seat.GetMicSessionId()
|
||||
}
|
||||
}
|
||||
return &roomv1.MicUpResponse{
|
||||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||||
SeatNo: result.seatNo,
|
||||
Room: result.snapshot,
|
||||
MicSessionId: result.micSessionID,
|
||||
PublishDeadlineMs: 0,
|
||||
}, nil
|
||||
}
|
||||
@ -81,6 +81,15 @@ func (w *rocketTestWallet) BatchDebitGift(_ context.Context, req *walletv1.Batch
|
||||
return &walletv1.BatchDebitGiftResponse{Aggregate: aggregate, Receipts: receipts}, nil
|
||||
}
|
||||
|
||||
func (w *rocketTestWallet) DebitRobotGift(_ context.Context, req *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error) {
|
||||
if len(w.debits) == 0 {
|
||||
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-robot-default", HeatValue: 1, CoinSpent: 1, ChargeAmount: 1, ChargeAssetType: "ROBOT_COIN"}, nil
|
||||
}
|
||||
next := w.debits[0]
|
||||
w.debits = w.debits[1:]
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func joinReceiptIDs(left string, right string) string {
|
||||
if left == "" {
|
||||
return right
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@ -89,6 +90,11 @@ type Service struct {
|
||||
mu sync.Mutex
|
||||
// cells 保存当前节点已经装载的单房间执行单元。
|
||||
cells map[string]*cell.RoomCell
|
||||
|
||||
// robotRuntimeMu 保护后台机器人房间运行循环;配置变更时要取消旧循环后重建。
|
||||
robotRuntimeMu sync.Mutex
|
||||
// robotRuntimes 保存 room_id -> cancel,避免同一房间被重复启动多个送礼循环。
|
||||
robotRuntimes map[string]context.CancelFunc
|
||||
}
|
||||
|
||||
// mutationResult 是命令在 Room Cell 内执行后的统一结果包。
|
||||
@ -203,6 +209,7 @@ func New(cfg Config, directory router.Directory, repository Repository, wallet i
|
||||
luckyGiftSendLockTTL: luckyGiftSendLockTTL,
|
||||
rtcUserRemover: cfg.RTCUserRemover,
|
||||
cells: make(map[string]*cell.RoomCell),
|
||||
robotRuntimes: make(map[string]context.CancelFunc),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -275,6 +275,30 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
PRIMARY KEY (app_code),
|
||||
KEY idx_room_rocket_enabled (app_code, enabled)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||
`CREATE TABLE IF NOT EXISTS room_robot_rooms (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
room_id VARCHAR(64) NOT NULL,
|
||||
room_short_id VARCHAR(32) NOT NULL DEFAULT '',
|
||||
title VARCHAR(128) NOT NULL DEFAULT '',
|
||||
cover_url VARCHAR(512) NOT NULL DEFAULT '',
|
||||
visible_region_id BIGINT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(32) NOT NULL,
|
||||
owner_robot_user_id BIGINT NOT NULL,
|
||||
robot_user_ids_json JSON NOT NULL,
|
||||
gift_ids_json JSON NOT NULL,
|
||||
lucky_gift_ids_json JSON NOT NULL,
|
||||
normal_gift_interval_ms BIGINT NOT NULL,
|
||||
lucky_combo_min BIGINT NOT NULL,
|
||||
lucky_combo_max BIGINT NOT NULL,
|
||||
lucky_pause_min_ms BIGINT NOT NULL,
|
||||
lucky_pause_max_ms BIGINT NOT NULL,
|
||||
created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
created_at_ms BIGINT NOT NULL,
|
||||
updated_at_ms BIGINT NOT NULL,
|
||||
PRIMARY KEY (app_code, room_id),
|
||||
KEY idx_room_robot_rooms_status (app_code, status, updated_at_ms, room_id),
|
||||
KEY idx_room_robot_rooms_owner (app_code, owner_robot_user_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`,
|
||||
`INSERT IGNORE INTO room_seat_configs (app_code, allowed_seat_counts, default_seat_count, created_at_ms, updated_at_ms)
|
||||
VALUES ('lalu', '[10,15,20,25,30]', 15, 0, 0)`,
|
||||
`INSERT IGNORE INTO room_rocket_configs (
|
||||
@ -325,6 +349,7 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
||||
`ALTER TABLE room_region_pins MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
|
||||
`ALTER TABLE room_seat_configs MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
|
||||
`ALTER TABLE room_rocket_configs MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
|
||||
`ALTER TABLE room_robot_rooms MODIFY COLUMN app_code VARCHAR(32) NOT NULL`,
|
||||
}
|
||||
|
||||
for _, statement := range statements {
|
||||
@ -1130,6 +1155,298 @@ func (r *Repository) UpsertRoomRocketConfig(ctx context.Context, config roomserv
|
||||
return err
|
||||
}
|
||||
|
||||
// ListRobotRooms 读取后台机器人房间列表;机器人房间配置归 room-service 所有,后台不直连房间库。
|
||||
func (r *Repository) ListRobotRooms(ctx context.Context, query roomservice.RobotRoomListQuery) ([]roomservice.RobotRoomConfig, int64, error) {
|
||||
appCode := normalizedRecordAppCode(ctx, query.AppCode)
|
||||
page := query.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := query.PageSize
|
||||
if pageSize <= 0 || pageSize > 100 {
|
||||
pageSize = 50
|
||||
}
|
||||
status := strings.TrimSpace(query.Status)
|
||||
args := []any{appCode}
|
||||
where := `WHERE app_code = ?`
|
||||
if status != "" {
|
||||
where += ` AND status = ?`
|
||||
args = append(args, status)
|
||||
}
|
||||
var total int64
|
||||
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM room_robot_rooms `+where, args...).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
args = append(args, pageSize, (page-1)*pageSize)
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT app_code, room_id, room_short_id, title, cover_url, visible_region_id, status,
|
||||
owner_robot_user_id, robot_user_ids_json, gift_ids_json, lucky_gift_ids_json,
|
||||
normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms,
|
||||
created_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM room_robot_rooms `+where+`
|
||||
ORDER BY updated_at_ms DESC, room_id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
args...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items, err := scanRobotRoomConfigs(rows)
|
||||
return items, total, err
|
||||
}
|
||||
|
||||
// GetRobotRoom 精确读取一个机器人房间配置。
|
||||
func (r *Repository) GetRobotRoom(ctx context.Context, roomID string) (roomservice.RobotRoomConfig, bool, error) {
|
||||
row := r.db.QueryRowContext(ctx,
|
||||
`SELECT app_code, room_id, room_short_id, title, cover_url, visible_region_id, status,
|
||||
owner_robot_user_id, robot_user_ids_json, gift_ids_json, lucky_gift_ids_json,
|
||||
normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms,
|
||||
created_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM room_robot_rooms
|
||||
WHERE app_code = ? AND room_id = ?`,
|
||||
appcode.FromContext(ctx),
|
||||
strings.TrimSpace(roomID),
|
||||
)
|
||||
item, err := scanRobotRoomConfig(row)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return roomservice.RobotRoomConfig{}, false, nil
|
||||
}
|
||||
return item, err == nil, err
|
||||
}
|
||||
|
||||
// CreateRobotRoomConfig 写入机器人房间配置,并用事务锁定候选账号避免两个 active 机器人房复用同一批账号。
|
||||
func (r *Repository) CreateRobotRoomConfig(ctx context.Context, input roomservice.CreateRobotRoomConfigInput) (roomservice.RobotRoomConfig, error) {
|
||||
config := input.Config
|
||||
appCode := normalizedRecordAppCode(ctx, config.AppCode)
|
||||
nowMS := input.NowMS
|
||||
if nowMS <= 0 {
|
||||
nowMS = time.Now().UTC().UnixMilli()
|
||||
}
|
||||
config.AppCode = appCode
|
||||
config.CreatedAtMS = firstPositiveInt64(config.CreatedAtMS, nowMS)
|
||||
config.UpdatedAtMS = firstPositiveInt64(config.UpdatedAtMS, nowMS)
|
||||
if config.Status == "" {
|
||||
config.Status = "active"
|
||||
}
|
||||
rawRobots, rawGifts, rawLuckyGifts, err := robotRoomJSON(config)
|
||||
if err != nil {
|
||||
return roomservice.RobotRoomConfig{}, err
|
||||
}
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return roomservice.RobotRoomConfig{}, err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
occupied, err := occupiedRobotUserIDsTx(ctx, tx, appCode, config.RobotUserIDs)
|
||||
if err != nil {
|
||||
return roomservice.RobotRoomConfig{}, err
|
||||
}
|
||||
if len(occupied) > 0 {
|
||||
return roomservice.RobotRoomConfig{}, xerr.New(xerr.Conflict, "robot user already assigned to active robot room")
|
||||
}
|
||||
_, err = tx.ExecContext(ctx,
|
||||
`INSERT INTO room_robot_rooms (
|
||||
app_code, room_id, room_short_id, title, cover_url, visible_region_id, status,
|
||||
owner_robot_user_id, robot_user_ids_json, gift_ids_json, lucky_gift_ids_json,
|
||||
normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms,
|
||||
created_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
config.AppCode,
|
||||
config.RoomID,
|
||||
config.RoomShortID,
|
||||
config.Title,
|
||||
config.CoverURL,
|
||||
config.VisibleRegionID,
|
||||
config.Status,
|
||||
config.OwnerRobotUserID,
|
||||
string(rawRobots),
|
||||
string(rawGifts),
|
||||
string(rawLuckyGifts),
|
||||
config.GiftRule.NormalGiftIntervalMS,
|
||||
config.GiftRule.LuckyComboMin,
|
||||
config.GiftRule.LuckyComboMax,
|
||||
config.GiftRule.LuckyPauseMinMS,
|
||||
config.GiftRule.LuckyPauseMaxMS,
|
||||
config.CreatedByAdminID,
|
||||
config.CreatedAtMS,
|
||||
config.UpdatedAtMS,
|
||||
)
|
||||
if err != nil {
|
||||
return roomservice.RobotRoomConfig{}, err
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
return roomservice.RobotRoomConfig{}, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// UpdateRobotRoomStatus 只切换机器人房间运行态;停止后账号会从“已占用”集合释放。
|
||||
func (r *Repository) UpdateRobotRoomStatus(ctx context.Context, roomID string, status string, nowMS int64) (roomservice.RobotRoomConfig, bool, error) {
|
||||
status = strings.TrimSpace(status)
|
||||
if nowMS <= 0 {
|
||||
nowMS = time.Now().UTC().UnixMilli()
|
||||
}
|
||||
res, err := r.db.ExecContext(ctx,
|
||||
`UPDATE room_robot_rooms
|
||||
SET status = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND room_id = ?`,
|
||||
status,
|
||||
nowMS,
|
||||
appcode.FromContext(ctx),
|
||||
strings.TrimSpace(roomID),
|
||||
)
|
||||
if err != nil {
|
||||
return roomservice.RobotRoomConfig{}, false, err
|
||||
}
|
||||
affected, _ := res.RowsAffected()
|
||||
if affected == 0 {
|
||||
return roomservice.RobotRoomConfig{}, false, nil
|
||||
}
|
||||
item, exists, err := r.GetRobotRoom(ctx, roomID)
|
||||
return item, exists, err
|
||||
}
|
||||
|
||||
// OccupiedRobotUserIDs 返回仍被 active 机器人房占用的机器人账号集合。
|
||||
func (r *Repository) OccupiedRobotUserIDs(ctx context.Context, userIDs []int64) (map[int64]bool, error) {
|
||||
return occupiedRobotUserIDsQuery(ctx, r.db, appcode.FromContext(ctx), userIDs, false)
|
||||
}
|
||||
|
||||
// ListActiveRobotRooms 为 room-service 重启恢复运行时循环提供当前 active 配置。
|
||||
func (r *Repository) ListActiveRobotRooms(ctx context.Context) ([]roomservice.RobotRoomConfig, error) {
|
||||
rows, err := r.db.QueryContext(ctx,
|
||||
`SELECT app_code, room_id, room_short_id, title, cover_url, visible_region_id, status,
|
||||
owner_robot_user_id, robot_user_ids_json, gift_ids_json, lucky_gift_ids_json,
|
||||
normal_gift_interval_ms, lucky_combo_min, lucky_combo_max, lucky_pause_min_ms, lucky_pause_max_ms,
|
||||
created_by_admin_id, created_at_ms, updated_at_ms
|
||||
FROM room_robot_rooms
|
||||
WHERE status = 'active'
|
||||
ORDER BY app_code ASC, updated_at_ms DESC, room_id DESC`,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanRobotRoomConfigs(rows)
|
||||
}
|
||||
|
||||
type robotRoomScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanRobotRoomConfigs(rows *sql.Rows) ([]roomservice.RobotRoomConfig, error) {
|
||||
items := make([]roomservice.RobotRoomConfig, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanRobotRoomConfig(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func scanRobotRoomConfig(row robotRoomScanner) (roomservice.RobotRoomConfig, error) {
|
||||
var item roomservice.RobotRoomConfig
|
||||
var rawRobots string
|
||||
var rawGifts string
|
||||
var rawLuckyGifts string
|
||||
if err := row.Scan(
|
||||
&item.AppCode,
|
||||
&item.RoomID,
|
||||
&item.RoomShortID,
|
||||
&item.Title,
|
||||
&item.CoverURL,
|
||||
&item.VisibleRegionID,
|
||||
&item.Status,
|
||||
&item.OwnerRobotUserID,
|
||||
&rawRobots,
|
||||
&rawGifts,
|
||||
&rawLuckyGifts,
|
||||
&item.GiftRule.NormalGiftIntervalMS,
|
||||
&item.GiftRule.LuckyComboMin,
|
||||
&item.GiftRule.LuckyComboMax,
|
||||
&item.GiftRule.LuckyPauseMinMS,
|
||||
&item.GiftRule.LuckyPauseMaxMS,
|
||||
&item.CreatedByAdminID,
|
||||
&item.CreatedAtMS,
|
||||
&item.UpdatedAtMS,
|
||||
); err != nil {
|
||||
return roomservice.RobotRoomConfig{}, err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(rawRobots), &item.RobotUserIDs); err != nil {
|
||||
return roomservice.RobotRoomConfig{}, err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(rawGifts), &item.GiftRule.GiftIDs); err != nil {
|
||||
return roomservice.RobotRoomConfig{}, err
|
||||
}
|
||||
if err := json.Unmarshal([]byte(rawLuckyGifts), &item.GiftRule.LuckyGiftIDs); err != nil {
|
||||
return roomservice.RobotRoomConfig{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func robotRoomJSON(config roomservice.RobotRoomConfig) ([]byte, []byte, []byte, error) {
|
||||
rawRobots, err := json.Marshal(config.RobotUserIDs)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
rawGifts, err := json.Marshal(config.GiftRule.GiftIDs)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
rawLuckyGifts, err := json.Marshal(config.GiftRule.LuckyGiftIDs)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return rawRobots, rawGifts, rawLuckyGifts, nil
|
||||
}
|
||||
|
||||
func occupiedRobotUserIDsTx(ctx context.Context, tx *sql.Tx, appCode string, userIDs []int64) (map[int64]bool, error) {
|
||||
return occupiedRobotUserIDsQuery(ctx, tx, appCode, userIDs, true)
|
||||
}
|
||||
|
||||
type dbQuerier interface {
|
||||
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
|
||||
}
|
||||
|
||||
func occupiedRobotUserIDsQuery(ctx context.Context, db dbQuerier, appCode string, userIDs []int64, lock bool) (map[int64]bool, error) {
|
||||
ids := uniquePositiveInt64(userIDs)
|
||||
occupied := make(map[int64]bool, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return occupied, nil
|
||||
}
|
||||
query := `SELECT robot_user_ids_json FROM room_robot_rooms WHERE app_code = ? AND status = 'active'`
|
||||
if lock {
|
||||
query += ` FOR UPDATE`
|
||||
}
|
||||
rows, err := db.QueryContext(ctx, query, appcode.Normalize(appCode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
wanted := make(map[int64]bool, len(ids))
|
||||
for _, id := range ids {
|
||||
wanted[id] = true
|
||||
}
|
||||
for rows.Next() {
|
||||
var raw string
|
||||
if err := rows.Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var roomRobotIDs []int64
|
||||
if err := json.Unmarshal([]byte(raw), &roomRobotIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, id := range roomRobotIDs {
|
||||
if wanted[id] {
|
||||
occupied[id] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return occupied, rows.Err()
|
||||
}
|
||||
|
||||
// ListCommandsAfter 返回快照版本之后的可回放命令日志。
|
||||
func (r *Repository) ListCommandsAfter(ctx context.Context, roomID string, roomVersion int64) ([]roomservice.CommandRecord, error) {
|
||||
// 恢复必须按 room_version 再按自增 id 排序,确保同版本异常数据也有稳定顺序。
|
||||
@ -2640,6 +2957,29 @@ func boolToInt(value bool) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func firstPositiveInt64(values ...int64) int64 {
|
||||
for _, value := range values {
|
||||
if value > 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func uniquePositiveInt64(values []int64) []int64 {
|
||||
seen := make(map[int64]bool, len(values))
|
||||
out := make([]int64, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value <= 0 || seen[value] {
|
||||
continue
|
||||
}
|
||||
seen[value] = true
|
||||
out = append(out, value)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
|
||||
return out
|
||||
}
|
||||
|
||||
func mapRoomMetaDuplicateError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
|
||||
@ -187,6 +187,16 @@ func (s *Server) AdminCancelRoomPin(ctx context.Context, req *roomv1.AdminCancel
|
||||
return mapServiceResult(s.svc.AdminCancelRoomPin(ctx, req))
|
||||
}
|
||||
|
||||
func (s *Server) AdminCreateRobotRoom(ctx context.Context, req *roomv1.AdminCreateRobotRoomRequest) (*roomv1.AdminCreateRobotRoomResponse, error) {
|
||||
ctx = contextWithMetaApp(ctx, req.GetMeta())
|
||||
return mapServiceResult(s.svc.AdminCreateRobotRoom(ctx, req))
|
||||
}
|
||||
|
||||
func (s *Server) AdminSetRobotRoomStatus(ctx context.Context, req *roomv1.AdminSetRobotRoomStatusRequest) (*roomv1.AdminSetRobotRoomStatusResponse, error) {
|
||||
ctx = contextWithMetaApp(ctx, req.GetMeta())
|
||||
return mapServiceResult(s.svc.AdminSetRobotRoomStatus(ctx, req))
|
||||
}
|
||||
|
||||
// MicUp 代理到领域服务。
|
||||
func (s *Server) MicUp(ctx context.Context, req *roomv1.MicUpRequest) (*roomv1.MicUpResponse, error) {
|
||||
// 麦位修改由 Room Cell 串行执行,gRPC 层不做状态判断。
|
||||
@ -443,6 +453,16 @@ func (s *Server) AdminListRoomPins(ctx context.Context, req *roomv1.AdminListRoo
|
||||
return mapServiceResult(s.svc.AdminListRoomPins(ctx, req))
|
||||
}
|
||||
|
||||
func (s *Server) AdminListRobotRooms(ctx context.Context, req *roomv1.AdminListRobotRoomsRequest) (*roomv1.AdminListRobotRoomsResponse, error) {
|
||||
ctx = contextWithMetaApp(ctx, req.GetMeta())
|
||||
return mapServiceResult(s.svc.AdminListRobotRooms(ctx, req))
|
||||
}
|
||||
|
||||
func (s *Server) AdminFilterAvailableRoomRobots(ctx context.Context, req *roomv1.AdminFilterAvailableRoomRobotsRequest) (*roomv1.AdminFilterAvailableRoomRobotsResponse, error) {
|
||||
ctx = contextWithMetaApp(ctx, req.GetMeta())
|
||||
return mapServiceResult(s.svc.AdminFilterAvailableRoomRobots(ctx, req))
|
||||
}
|
||||
|
||||
// ListRooms 代理到 room-service 列表读模型。
|
||||
func (s *Server) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (*roomv1.ListRoomsResponse, error) {
|
||||
// 列表查询只读 MySQL 投影,不扫描 Room Cell 内存集合。
|
||||
|
||||
@ -35,6 +35,11 @@ func (fakeWallet) BatchDebitGift(context.Context, *walletv1.BatchDebitGiftReques
|
||||
return &walletv1.BatchDebitGiftResponse{Aggregate: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-test"}}, nil
|
||||
}
|
||||
|
||||
// DebitRobotGift 返回固定结果,避免 transport 测试依赖 wallet-service 机器人金币实现。
|
||||
func (fakeWallet) DebitRobotGift(context.Context, *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error) {
|
||||
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-robot-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
|
||||
|
||||
@ -371,6 +371,9 @@ func roomGiftEvent(body []byte) (mysqlstorage.RoomGiftEvent, bool, error) {
|
||||
if err := proto.Unmarshal(envelope.GetBody(), &gift); err != nil {
|
||||
return mysqlstorage.RoomGiftEvent{}, false, err
|
||||
}
|
||||
if gift.GetIsRobotGift() {
|
||||
return mysqlstorage.RoomGiftEvent{}, false, nil
|
||||
}
|
||||
return mysqlstorage.RoomGiftEvent{
|
||||
AppCode: envelope.GetAppCode(),
|
||||
EventID: envelope.GetEventId(),
|
||||
|
||||
@ -7,6 +7,8 @@ const (
|
||||
AssetCoin = "COIN"
|
||||
// AssetCoinSellerCoin 是币商专用金币资产,只能通过币商转账兑换成玩家普通 COIN。
|
||||
AssetCoinSellerCoin = "COIN_SELLER_COIN"
|
||||
// AssetRobotCoin 是机器人房间专用金币资产,只允许内部机器人送礼链路扣减,不进入真人充值消费口径。
|
||||
AssetRobotCoin = "ROBOT_COIN"
|
||||
// AssetGiftPoint 是历史礼物积分资产,只保留历史账本查询兼容;新送礼不会再给它入账。
|
||||
AssetGiftPoint = "GIFT_POINT"
|
||||
// AssetHostPeriodDiamond 是主播工资周期钻石投影,只参与工资等级结算,不进入通用钱包余额。
|
||||
@ -146,6 +148,8 @@ type DebitGiftCommand struct {
|
||||
TargetHostRegionID int64
|
||||
// TargetAgencyOwnerUserID 是送礼瞬间主播上级代理的收款用户快照;结算按快照发放,避免月底组织变化错账。
|
||||
TargetAgencyOwnerUserID int64
|
||||
// RobotGift 表示本次扣费只服务机器人房间展示:扣 ROBOT_COIN,不给主播钻石,不进入真实礼物墙投影。
|
||||
RobotGift bool
|
||||
}
|
||||
|
||||
// DebitGiftTargetCommand 是批量送礼中单个接收方的账务语义。
|
||||
@ -1127,7 +1131,7 @@ type RedPacketExpireResult struct {
|
||||
// ValidAssetType 限定账本允许落账的资产类型,避免写入任意字符串资产。
|
||||
func ValidAssetType(assetType string) bool {
|
||||
switch assetType {
|
||||
case AssetCoin, AssetCoinSellerCoin, AssetGiftPoint, AssetHostSalaryUSD, AssetAgencySalaryUSD, AssetBDSalaryUSD, AssetAdminSalaryUSD:
|
||||
case AssetCoin, AssetCoinSellerCoin, AssetRobotCoin, AssetGiftPoint, AssetHostSalaryUSD, AssetAgencySalaryUSD, AssetBDSalaryUSD, AssetAdminSalaryUSD:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@ -164,6 +164,20 @@ func (s *Service) SetExternalRechargeConfig(config ExternalRechargeConfig) {
|
||||
|
||||
// DebitGift 校验送礼扣费命令,并交给 repository 做原子落账和幂等。
|
||||
func (s *Service) DebitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) {
|
||||
command.RobotGift = false
|
||||
return s.debitGift(ctx, command)
|
||||
}
|
||||
|
||||
// DebitRobotGift 校验机器人房间专用送礼扣费命令,并强制使用 ROBOT_COIN 账务语义。
|
||||
func (s *Service) DebitRobotGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) {
|
||||
command.RobotGift = true
|
||||
command.TargetIsHost = false
|
||||
command.TargetHostRegionID = 0
|
||||
command.TargetAgencyOwnerUserID = 0
|
||||
return s.debitGift(ctx, command)
|
||||
}
|
||||
|
||||
func (s *Service) debitGift(ctx context.Context, command ledger.DebitGiftCommand) (ledger.Receipt, error) {
|
||||
if command.CommandID == "" || command.RoomID == "" || command.SenderUserID <= 0 || command.TargetUserID <= 0 || command.GiftID == "" {
|
||||
return ledger.Receipt{}, xerr.New(xerr.InvalidArgument, "billing command is incomplete")
|
||||
}
|
||||
|
||||
@ -24,6 +24,7 @@ import (
|
||||
const (
|
||||
transactionStatusSucceeded = "succeeded"
|
||||
bizTypeGiftDebit = "gift_debit"
|
||||
bizTypeRobotGiftDebit = "robot_gift_debit"
|
||||
bizTypeManualCredit = "manual_credit"
|
||||
bizTypeTaskReward = "task_reward"
|
||||
bizTypeLuckyGiftReward = "lucky_gift_reward"
|
||||
@ -365,7 +366,8 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
||||
}()
|
||||
|
||||
requestHash := debitRequestHash(command)
|
||||
if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizTypeGiftDebit); err != nil || exists {
|
||||
bizType := giftDebitBizType(command)
|
||||
if txRow, exists, err := r.lookupTransaction(ctx, tx, command.CommandID, requestHash, bizType); err != nil || exists {
|
||||
if err != nil || !exists {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
@ -388,6 +390,11 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
||||
if err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
chargeAssetType := price.ChargeAssetType
|
||||
if command.RobotGift {
|
||||
// 机器人礼物复用真实礼物价格和素材快照,但扣专用 ROBOT_COIN,避免污染真人 COIN 充值消费口径。
|
||||
chargeAssetType = ledger.AssetRobotCoin
|
||||
}
|
||||
chargeAmount, err := checkedMul(price.CoinPrice, int64(command.GiftCount))
|
||||
if err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
@ -406,7 +413,7 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
||||
hostPeriodCycleKey := ""
|
||||
giftDiamondRatioPercent := "0.00"
|
||||
giftDiamondRatioRegionID := int64(0)
|
||||
if command.TargetIsHost {
|
||||
if command.TargetIsHost && !command.RobotGift {
|
||||
ratio, ratioRegionID, err := r.resolveGlobalGiftDiamondRatio(ctx, tx, command.AppCode, giftConfig.GiftTypeCode)
|
||||
if err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
@ -421,13 +428,19 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
||||
hostPeriodCycleKey = hostPeriodCycleKeyFromMS(nowMs)
|
||||
}
|
||||
|
||||
sender, err := r.lockAccount(ctx, tx, command.SenderUserID, price.ChargeAssetType, chargeAmount == 0, nowMs)
|
||||
sender, err := r.lockAccount(ctx, tx, command.SenderUserID, chargeAssetType, chargeAmount == 0, nowMs)
|
||||
if err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
if sender.AvailableAmount < chargeAmount {
|
||||
robotCoinTopUp := int64(0)
|
||||
if command.RobotGift && sender.AvailableAmount < chargeAmount {
|
||||
// 机器人房间使用专用 ROBOT_COIN 做展示账务,按次补足后立刻扣减,不要求真实充值预算。
|
||||
robotCoinTopUp = chargeAmount - sender.AvailableAmount
|
||||
}
|
||||
if !command.RobotGift && sender.AvailableAmount < chargeAmount {
|
||||
return ledger.Receipt{}, xerr.New(xerr.InsufficientBalance, "insufficient balance")
|
||||
}
|
||||
senderAfter := sender.AvailableAmount + robotCoinTopUp - chargeAmount
|
||||
transactionID := transactionID(command.AppCode, command.CommandID)
|
||||
metadata := giftMetadata{
|
||||
AppCode: command.AppCode,
|
||||
@ -445,12 +458,12 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
||||
SortOrder: giftConfig.SortOrder,
|
||||
GiftCount: command.GiftCount,
|
||||
PriceVersion: price.PriceVersion,
|
||||
ChargeAssetType: price.ChargeAssetType,
|
||||
ChargeAssetType: chargeAssetType,
|
||||
ChargeAmount: chargeAmount,
|
||||
CoinSpent: chargeAmount,
|
||||
GiftPointAdded: 0,
|
||||
HeatValue: heatValue,
|
||||
BalanceAfter: sender.AvailableAmount - chargeAmount,
|
||||
BalanceAfter: senderAfter,
|
||||
BillingReceipt: billingReceiptID(command.AppCode, command.CommandID),
|
||||
SenderUserID: command.SenderUserID,
|
||||
SenderRegionID: command.SenderRegionID,
|
||||
@ -469,18 +482,42 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
||||
CoinPrice: price.CoinPrice,
|
||||
GiftPointAmount: 0,
|
||||
HeatUnitValue: price.CoinPrice,
|
||||
RobotGift: command.RobotGift,
|
||||
}
|
||||
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizTypeGiftDebit, requestHash, fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil {
|
||||
if err := r.insertTransaction(ctx, tx, transactionID, command.CommandID, bizType, requestHash, fmt.Sprintf("%s:%s", command.GiftID, price.PriceVersion), metadata, nowMs); err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
events := make([]walletOutboxEvent, 0, 3)
|
||||
if robotCoinTopUp > 0 {
|
||||
if err := r.applyAccountDelta(ctx, tx, sender, robotCoinTopUp, 0, nowMs); err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
topUpAfter := sender.AvailableAmount + robotCoinTopUp
|
||||
if err := r.insertEntry(ctx, tx, walletEntry{
|
||||
TransactionID: transactionID,
|
||||
UserID: command.SenderUserID,
|
||||
AssetType: chargeAssetType,
|
||||
AvailableDelta: robotCoinTopUp,
|
||||
FrozenDelta: 0,
|
||||
AvailableAfter: topUpAfter,
|
||||
FrozenAfter: sender.FrozenAmount,
|
||||
CounterpartyUserID: 0,
|
||||
RoomID: command.RoomID,
|
||||
CreatedAtMS: nowMs,
|
||||
}); err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
events = append(events, balanceChangedEvent(transactionID, command.CommandID, command.SenderUserID, chargeAssetType, robotCoinTopUp, 0, topUpAfter, sender.FrozenAmount, sender.Version+1, metadata, nowMs))
|
||||
sender.AvailableAmount = topUpAfter
|
||||
sender.Version++
|
||||
}
|
||||
if err := r.applyAccountDelta(ctx, tx, sender, -chargeAmount, 0, nowMs); err != nil {
|
||||
return ledger.Receipt{}, err
|
||||
}
|
||||
senderAfter := sender.AvailableAmount - chargeAmount
|
||||
if err := r.insertEntry(ctx, tx, walletEntry{
|
||||
TransactionID: transactionID,
|
||||
UserID: command.SenderUserID,
|
||||
AssetType: price.ChargeAssetType,
|
||||
AssetType: chargeAssetType,
|
||||
AvailableDelta: -chargeAmount,
|
||||
FrozenDelta: 0,
|
||||
AvailableAfter: senderAfter,
|
||||
@ -501,9 +538,10 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
|
||||
metadata.HostPeriodDiamondVersion = hostPeriodDiamondVersion
|
||||
}
|
||||
|
||||
events := []walletOutboxEvent{
|
||||
balanceChangedEvent(transactionID, command.CommandID, command.SenderUserID, price.ChargeAssetType, -chargeAmount, 0, senderAfter, sender.FrozenAmount, sender.Version+1, metadata, nowMs),
|
||||
giftDebitedEvent(transactionID, command.CommandID, command.SenderUserID, price.ChargeAssetType, -chargeAmount, 0, metadata, nowMs),
|
||||
events = append(events, balanceChangedEvent(transactionID, command.CommandID, command.SenderUserID, chargeAssetType, -chargeAmount, 0, senderAfter, sender.FrozenAmount, sender.Version+1, metadata, nowMs))
|
||||
if !command.RobotGift {
|
||||
// 只有真人礼物产生 WalletGiftDebited;机器人礼物不投影礼物墙,也不进入真实消费类下游。
|
||||
events = append(events, giftDebitedEvent(transactionID, command.CommandID, command.SenderUserID, chargeAssetType, -chargeAmount, 0, metadata, nowMs))
|
||||
}
|
||||
if hostPeriodDiamondAdded > 0 {
|
||||
events = append(events, hostPeriodDiamondCreditedEvent(transactionID, command.CommandID, metadata, nowMs))
|
||||
@ -809,6 +847,10 @@ func (r *Repository) GetBalances(ctx context.Context, userID int64, assetTypes [
|
||||
}
|
||||
|
||||
func (r *Repository) upsertUserGiftWall(ctx context.Context, tx *sql.Tx, metadata giftMetadata, nowMs int64) error {
|
||||
if metadata.RobotGift {
|
||||
// 机器人房间礼物只服务房间展示,不进入真人用户礼物墙。
|
||||
return nil
|
||||
}
|
||||
chargeAssetType := ledger.NormalizeGiftChargeAssetType(metadata.ChargeAssetType)
|
||||
chargeAmount := metadata.ChargeAmount
|
||||
if chargeAmount == 0 {
|
||||
@ -2957,6 +2999,7 @@ type giftMetadata struct {
|
||||
RoomRegionID int64 `json:"room_region_id"`
|
||||
RoomContributionRatioPercent string `json:"room_contribution_ratio_percent"`
|
||||
RoomContributionRatioRegionID int64 `json:"room_contribution_ratio_region_id"`
|
||||
RobotGift bool `json:"robot_gift,omitempty"`
|
||||
}
|
||||
|
||||
type adminCreditMetadata struct {
|
||||
@ -3137,6 +3180,9 @@ type coinSellerStockMetadata struct {
|
||||
|
||||
func receiptFromGiftMetadata(transactionID string, metadata giftMetadata) ledger.Receipt {
|
||||
chargeAssetType := ledger.NormalizeGiftChargeAssetType(metadata.ChargeAssetType)
|
||||
if metadata.RobotGift {
|
||||
chargeAssetType = ledger.AssetRobotCoin
|
||||
}
|
||||
chargeAmount := metadata.ChargeAmount
|
||||
if chargeAmount == 0 {
|
||||
chargeAmount = metadata.CoinSpent
|
||||
@ -3831,6 +3877,13 @@ func debitGiftCommandFromBatchTarget(command ledger.BatchDebitGiftCommand, targe
|
||||
}
|
||||
}
|
||||
|
||||
func giftDebitBizType(command ledger.DebitGiftCommand) string {
|
||||
if command.RobotGift {
|
||||
return bizTypeRobotGiftDebit
|
||||
}
|
||||
return bizTypeGiftDebit
|
||||
}
|
||||
|
||||
func aggregateGiftReceipts(targets []ledger.BatchGiftTargetReceipt) (ledger.Receipt, error) {
|
||||
aggregate := ledger.Receipt{}
|
||||
billingReceiptIDs := make([]string, 0, len(targets))
|
||||
@ -3897,7 +3950,7 @@ func hostPeriodCycleKeyFromMS(ms int64) string {
|
||||
|
||||
func debitRequestHash(command ledger.DebitGiftCommand) string {
|
||||
// 哈希覆盖所有账务语义字段;相同 command_id 只能重放完全相同的业务命令。
|
||||
return stableHash(fmt.Sprintf("gift|%s|%s|%d|%d|%s|%d|%s|%d|%d|%t|%d|%d",
|
||||
return stableHash(fmt.Sprintf("gift|%s|%s|%d|%d|%s|%d|%s|%d|%d|%t|%d|%d|%t",
|
||||
appcode.Normalize(command.AppCode),
|
||||
command.RoomID,
|
||||
command.SenderUserID,
|
||||
@ -3910,6 +3963,7 @@ func debitRequestHash(command ledger.DebitGiftCommand) string {
|
||||
command.TargetIsHost,
|
||||
command.TargetHostRegionID,
|
||||
command.TargetAgencyOwnerUserID,
|
||||
command.RobotGift,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -91,6 +91,27 @@ func (s *Server) BatchDebitGift(ctx context.Context, req *walletv1.BatchDebitGif
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// DebitRobotGift 扣机器人专用金币;该入口只供内部机器人房间编排链路调用。
|
||||
func (s *Server) DebitRobotGift(ctx context.Context, req *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error) {
|
||||
ctx = appcode.WithContext(ctx, req.GetAppCode())
|
||||
receipt, err := s.svc.DebitRobotGift(ctx, ledger.DebitGiftCommand{
|
||||
AppCode: req.GetAppCode(),
|
||||
CommandID: req.GetCommandId(),
|
||||
RoomID: req.GetRoomId(),
|
||||
SenderUserID: req.GetSenderUserId(),
|
||||
TargetUserID: req.GetTargetUserId(),
|
||||
GiftID: req.GetGiftId(),
|
||||
GiftCount: req.GetGiftCount(),
|
||||
PriceVersion: req.GetPriceVersion(),
|
||||
RegionID: req.GetRegionId(),
|
||||
SenderRegionID: req.GetSenderRegionId(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
return debitGiftResponseFromReceipt(receipt), nil
|
||||
}
|
||||
|
||||
func debitGiftResponseFromReceipt(receipt ledger.Receipt) *walletv1.DebitGiftResponse {
|
||||
return &walletv1.DebitGiftResponse{
|
||||
BillingReceiptId: receipt.BillingReceiptID,
|
||||
|
||||
484
tools/robot-room-api-test/main.go
Normal file
484
tools/robot-room-api-test/main.go
Normal file
@ -0,0 +1,484 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAdminBaseURL = "http://127.0.0.1:13100/api/v1"
|
||||
defaultAdminUser = "admin"
|
||||
defaultAdminPass = "admin123"
|
||||
defaultMySQLDSN = "hyapp:hyapp@tcp(127.0.0.1:23306)/%s?parseTime=true&charset=utf8mb4&loc=UTC"
|
||||
|
||||
appCode = "lalu"
|
||||
roomScene = "voice"
|
||||
normalGiftID = "robot_room_test_normal"
|
||||
luckyGiftID = "robot_room_test_lucky"
|
||||
robotUserStart = int64(920001)
|
||||
robotUserCount = 12
|
||||
)
|
||||
|
||||
type apiBody struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type loginData struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
}
|
||||
|
||||
type availableRobot struct {
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
|
||||
type robotRoom struct {
|
||||
RoomID string `json:"roomId"`
|
||||
Status string `json:"status"`
|
||||
OwnerRobotUserID string `json:"ownerRobotUserId"`
|
||||
RobotUserIDs []string `json:"robotUserIds"`
|
||||
GiftRule struct {
|
||||
GiftIDs []string `json:"giftIds"`
|
||||
LuckyGiftIDs []string `json:"luckyGiftIds"`
|
||||
NormalGiftIntervalMS int64 `json:"normalGiftIntervalMs"`
|
||||
LuckyComboMin int64 `json:"luckyComboMin"`
|
||||
LuckyComboMax int64 `json:"luckyComboMax"`
|
||||
LuckyPauseMinMS int64 `json:"luckyPauseMinMs"`
|
||||
LuckyPauseMaxMS int64 `json:"luckyPauseMaxMs"`
|
||||
} `json:"giftRule"`
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
Items []robotRoom `json:"items"`
|
||||
Total int64 `json:"total"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
|
||||
baseURL := env("ROBOT_ROOM_ADMIN_BASE_URL", defaultAdminBaseURL)
|
||||
mysqlDSN := env("ROBOT_ROOM_MYSQL_DSN", defaultMySQLDSN)
|
||||
adminUser := env("ROBOT_ROOM_ADMIN_USERNAME", defaultAdminUser)
|
||||
adminPass := env("ROBOT_ROOM_ADMIN_PASSWORD", defaultAdminPass)
|
||||
|
||||
dbs, err := openDBs(ctx, mysqlDSN)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
defer dbs.close()
|
||||
|
||||
if err := seed(ctx, dbs); err != nil {
|
||||
fail(fmt.Errorf("seed local data: %w", err))
|
||||
}
|
||||
fmt.Println("seed: ok")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
token, err := login(ctx, client, baseURL, adminUser, adminPass)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
fmt.Println("login: ok")
|
||||
|
||||
available, err := getJSON[[]availableRobot](ctx, client, baseURL+"/admin/rooms/robot-rooms/available-robots", token)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
if len(available) < 10 {
|
||||
fail(fmt.Errorf("available robots = %d, want at least 10", len(available)))
|
||||
}
|
||||
fmt.Printf("available robots: %d\n", len(available))
|
||||
|
||||
payload := map[string]any{
|
||||
"ownerRobotUserId": robotUserStart,
|
||||
"candidateRobotUserIds": robotIDs(),
|
||||
"minRobotCount": 6,
|
||||
"maxRobotCount": 8,
|
||||
"roomName": "本地接口测试机器人房",
|
||||
"visibleRegionId": 0,
|
||||
"giftIds": []string{normalGiftID},
|
||||
"luckyGiftIds": []string{luckyGiftID},
|
||||
"normalGiftIntervalMs": 60000,
|
||||
"luckyComboMin": 2,
|
||||
"luckyComboMax": 3,
|
||||
"luckyPauseMinMs": 600000,
|
||||
"luckyPauseMaxMs": 600000,
|
||||
}
|
||||
created, err := postJSON[robotRoom](ctx, client, baseURL+"/admin/rooms/robot-rooms", token, payload)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
if created.RoomID == "" || created.Status != "active" {
|
||||
fail(fmt.Errorf("created room invalid: room_id=%q status=%q", created.RoomID, created.Status))
|
||||
}
|
||||
if len(created.RobotUserIDs) < 6 || len(created.RobotUserIDs) > 8 {
|
||||
fail(fmt.Errorf("created robot count = %d, want 6-8", len(created.RobotUserIDs)))
|
||||
}
|
||||
fmt.Printf("create robot room: %s robots=%d\n", created.RoomID, len(created.RobotUserIDs))
|
||||
|
||||
list, err := getJSON[pageData](ctx, client, baseURL+"/admin/rooms/robot-rooms?status=active&page=1&page_size=20", token)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
if !containsRoom(list.Items, created.RoomID) {
|
||||
fail(fmt.Errorf("created room %s not found in active list", created.RoomID))
|
||||
}
|
||||
fmt.Printf("list active robot rooms: total=%d\n", list.Total)
|
||||
|
||||
stopped, err := postJSON[robotRoom](ctx, client, baseURL+"/admin/rooms/robot-rooms/"+created.RoomID+"/stop", token, nil)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
if stopped.Status != "stopped" {
|
||||
fail(fmt.Errorf("stop status = %q, want stopped", stopped.Status))
|
||||
}
|
||||
fmt.Println("stop robot room: ok")
|
||||
|
||||
started, err := postJSON[robotRoom](ctx, client, baseURL+"/admin/rooms/robot-rooms/"+created.RoomID+"/start", token, nil)
|
||||
if err != nil {
|
||||
fail(err)
|
||||
}
|
||||
if started.Status != "active" {
|
||||
fail(fmt.Errorf("start status = %q, want active", started.Status))
|
||||
}
|
||||
fmt.Println("start robot room: ok")
|
||||
|
||||
if _, err := postJSON[robotRoom](ctx, client, baseURL+"/admin/rooms/robot-rooms/"+created.RoomID+"/stop", token, nil); err != nil {
|
||||
fail(err)
|
||||
}
|
||||
if err := verifyDB(ctx, dbs, created.RoomID); err != nil {
|
||||
fail(err)
|
||||
}
|
||||
fmt.Println("db verify: ok")
|
||||
fmt.Println("PASS robot room admin API smoke")
|
||||
}
|
||||
|
||||
type dbs struct {
|
||||
user *sql.DB
|
||||
wallet *sql.DB
|
||||
robot *sql.DB
|
||||
room *sql.DB
|
||||
}
|
||||
|
||||
func openDBs(ctx context.Context, dsnTemplate string) (dbs, error) {
|
||||
open := func(name string) (*sql.DB, error) {
|
||||
dsn := fmt.Sprintf(dsnTemplate, name)
|
||||
if !strings.Contains(dsnTemplate, "%s") {
|
||||
dsn = dsnTemplate
|
||||
}
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := db.PingContext(ctx); err != nil {
|
||||
_ = db.Close()
|
||||
return nil, fmt.Errorf("%s: %w", name, err)
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
userDB, err := open("hyapp_user")
|
||||
if err != nil {
|
||||
return dbs{}, err
|
||||
}
|
||||
walletDB, err := open("hyapp_wallet")
|
||||
if err != nil {
|
||||
_ = userDB.Close()
|
||||
return dbs{}, err
|
||||
}
|
||||
robotDB, err := open("hyapp_robot")
|
||||
if err != nil {
|
||||
_ = userDB.Close()
|
||||
_ = walletDB.Close()
|
||||
return dbs{}, err
|
||||
}
|
||||
roomDB, err := open("hyapp_room")
|
||||
if err != nil {
|
||||
_ = userDB.Close()
|
||||
_ = walletDB.Close()
|
||||
_ = robotDB.Close()
|
||||
return dbs{}, err
|
||||
}
|
||||
return dbs{user: userDB, wallet: walletDB, robot: robotDB, room: roomDB}, nil
|
||||
}
|
||||
|
||||
func (d dbs) close() {
|
||||
_ = d.user.Close()
|
||||
_ = d.wallet.Close()
|
||||
_ = d.robot.Close()
|
||||
_ = d.room.Close()
|
||||
}
|
||||
|
||||
func seed(ctx context.Context, dbs dbs) error {
|
||||
now := time.Now().UnixMilli()
|
||||
ids := robotIDs()
|
||||
args := make([]any, 0, len(ids)+1)
|
||||
args = append(args, appCode)
|
||||
in := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
in = append(in, "?")
|
||||
args = append(args, id)
|
||||
}
|
||||
idClause := strings.Join(in, ",")
|
||||
for _, statement := range []struct {
|
||||
db *sql.DB
|
||||
sql string
|
||||
args []any
|
||||
}{
|
||||
{dbs.room, "DELETE FROM room_command_log WHERE app_code = ? AND (room_id LIKE 'robot\\_%' OR command_id LIKE 'admin-robot-room:%')", []any{appCode}},
|
||||
{dbs.room, "DELETE FROM room_snapshots WHERE app_code = ? AND room_id LIKE 'robot\\_%'", []any{appCode}},
|
||||
{dbs.room, "DELETE FROM room_outbox WHERE app_code = ? AND room_id LIKE 'robot\\_%'", []any{appCode}},
|
||||
{dbs.room, "DELETE FROM room_robot_rooms WHERE app_code = ? AND owner_robot_user_id IN (" + idClause + ")", args},
|
||||
{dbs.room, "DELETE FROM rooms WHERE app_code = ? AND owner_user_id IN (" + idClause + ")", args},
|
||||
{dbs.room, "DELETE FROM room_list_entries WHERE app_code = ? AND owner_user_id IN (" + idClause + ")", args},
|
||||
{dbs.room, "DELETE FROM room_user_presence WHERE app_code = ? AND user_id IN (" + idClause + ")", args},
|
||||
} {
|
||||
if _, err := statement.db.ExecContext(ctx, statement.sql, statement.args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
display := strconv.FormatInt(id, 10)
|
||||
if _, err := dbs.user.ExecContext(ctx, `
|
||||
INSERT INTO users (
|
||||
app_code, user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind,
|
||||
username, gender, country, region_id, avatar, profile_completed, onboarding_status, status,
|
||||
created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, 'default', ?, 'unknown', 'SA', 0, '', 1, 'completed', 'active', ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
username = VALUES(username),
|
||||
current_display_user_id = VALUES(current_display_user_id),
|
||||
status = 'active',
|
||||
updated_at_ms = VALUES(updated_at_ms)`,
|
||||
appCode, id, display, display, fmt.Sprintf("机器人%d", id), now, now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := dbs.robot.ExecContext(ctx, `
|
||||
INSERT INTO robot_room_robots (
|
||||
app_code, room_scene, user_id, status, created_by_admin_id, created_at_ms, updated_at_ms, last_used_at_ms, used_count
|
||||
) VALUES (?, ?, ?, 'active', 1, ?, ?, 0, 0)
|
||||
ON DUPLICATE KEY UPDATE status = 'active', updated_at_ms = VALUES(updated_at_ms)`,
|
||||
appCode, roomScene, id, now, now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := seedGift(ctx, dbs.wallet, normalGiftID, "机器人普通测试礼物", 1001, 1, now); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedGift(ctx, dbs.wallet, luckyGiftID, "机器人幸运测试礼物", 1002, 2, now); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := dbs.wallet.ExecContext(ctx, `
|
||||
INSERT INTO gift_diamond_ratio_configs (
|
||||
app_code, region_id, gift_type_code, status, ratio_percent, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, 0, 'normal', 'active', 100.00, 1, 1, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE status = 'active', ratio_percent = VALUES(ratio_percent), updated_at_ms = VALUES(updated_at_ms)`,
|
||||
appCode, now, now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedGift(ctx context.Context, db *sql.DB, giftID string, name string, resourceID int64, coinPrice int64, now int64) error {
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
INSERT INTO resources (
|
||||
resource_id, app_code, resource_code, resource_type, name, status, grantable, manager_grant_enabled,
|
||||
grant_strategy, wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount,
|
||||
usage_scope_json, asset_url, preview_url, animation_url, metadata_json, sort_order,
|
||||
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, 'gift', ?, 'active', true, false, 'none', '', 0, 'coin', ?, 0, '{}', '', '', '', '{}', 0, 1, 1, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE name = VALUES(name), status = 'active', coin_price = VALUES(coin_price), updated_at_ms = VALUES(updated_at_ms)`,
|
||||
resourceID, appCode, giftID+"_resource", name, coinPrice, now, now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
INSERT INTO gift_type_configs (
|
||||
app_code, type_code, name, tab_key, status, sort_order, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, 'normal', '普通礼物', 'normal', 'active', 0, 1, 1, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE status = 'active', updated_at_ms = VALUES(updated_at_ms)`,
|
||||
appCode, now, now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
INSERT INTO gift_configs (
|
||||
app_code, gift_id, resource_id, status, name, sort_order, presentation_json, gift_type_code,
|
||||
effective_from_ms, effective_to_ms, effect_types_json, created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, 'active', ?, 0, '{}', 'normal', 0, 0, '["global_broadcast"]', 1, 1, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE status = 'active', name = VALUES(name), resource_id = VALUES(resource_id), updated_at_ms = VALUES(updated_at_ms)`,
|
||||
appCode, giftID, resourceID, name, now, now,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `
|
||||
INSERT INTO wallet_gift_prices (
|
||||
app_code, gift_id, price_version, status, charge_asset_type, coin_price, gift_point_amount, heat_value,
|
||||
effective_at_ms, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, 'robot_room_test_v1', 'active', 'COIN', ?, 0, ?, 0, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE status = 'active', coin_price = VALUES(coin_price), heat_value = VALUES(heat_value), updated_at_ms = VALUES(updated_at_ms)`,
|
||||
appCode, giftID, coinPrice, coinPrice, now, now,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func verifyDB(ctx context.Context, dbs dbs, roomID string) error {
|
||||
var status string
|
||||
var owner int64
|
||||
var rawRobots string
|
||||
err := dbs.room.QueryRowContext(ctx, `
|
||||
SELECT status, owner_robot_user_id, CAST(robot_user_ids_json AS CHAR)
|
||||
FROM room_robot_rooms
|
||||
WHERE app_code = ? AND room_id = ?`,
|
||||
appCode, roomID,
|
||||
).Scan(&status, &owner, &rawRobots)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status != "stopped" {
|
||||
return fmt.Errorf("db status = %s, want stopped", status)
|
||||
}
|
||||
if owner != robotUserStart {
|
||||
return fmt.Errorf("db owner = %d, want %d", owner, robotUserStart)
|
||||
}
|
||||
var robotIDs []int64
|
||||
if err := json.Unmarshal([]byte(rawRobots), &robotIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(robotIDs) < 6 || len(robotIDs) > 8 {
|
||||
return fmt.Errorf("db robot count = %d, want 6-8", len(robotIDs))
|
||||
}
|
||||
var realGiftDebits int64
|
||||
if err := dbs.wallet.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM wallet_transactions
|
||||
WHERE app_code = ? AND biz_type = 'gift_debit' AND JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.room_id')) = ?`,
|
||||
appCode, roomID,
|
||||
).Scan(&realGiftDebits); err != nil {
|
||||
return err
|
||||
}
|
||||
if realGiftDebits != 0 {
|
||||
return fmt.Errorf("real gift debit rows for robot room = %d, want 0", realGiftDebits)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func login(ctx context.Context, client *http.Client, baseURL string, username string, password string) (string, error) {
|
||||
data, err := postJSON[loginData](ctx, client, baseURL+"/auth/login", "", map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.TrimSpace(data.AccessToken) == "" {
|
||||
return "", errors.New("login returned empty access token")
|
||||
}
|
||||
return data.AccessToken, nil
|
||||
}
|
||||
|
||||
func getJSON[T any](ctx context.Context, client *http.Client, url string, token string) (T, error) {
|
||||
var zero T
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
return doJSON[T](client, req, token)
|
||||
}
|
||||
|
||||
func postJSON[T any](ctx context.Context, client *http.Client, url string, token string, payload any) (T, error) {
|
||||
var zero T
|
||||
var body io.Reader
|
||||
if payload != nil {
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
body = bytes.NewReader(raw)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
return doJSON[T](client, req, token)
|
||||
}
|
||||
|
||||
func doJSON[T any](client *http.Client, req *http.Request, token string) (T, error) {
|
||||
var zero T
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return zero, fmt.Errorf("%s %s: status %d body %s", req.Method, req.URL.Path, resp.StatusCode, strings.TrimSpace(string(raw)))
|
||||
}
|
||||
var wrapped apiBody
|
||||
if err := json.Unmarshal(raw, &wrapped); err != nil {
|
||||
return zero, err
|
||||
}
|
||||
if wrapped.Code != 0 {
|
||||
return zero, fmt.Errorf("%s %s: code %d message %s", req.Method, req.URL.Path, wrapped.Code, wrapped.Message)
|
||||
}
|
||||
var out T
|
||||
if len(wrapped.Data) == 0 || string(wrapped.Data) == "null" {
|
||||
return out, nil
|
||||
}
|
||||
if err := json.Unmarshal(wrapped.Data, &out); err != nil {
|
||||
return zero, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func robotIDs() []int64 {
|
||||
ids := make([]int64, 0, robotUserCount)
|
||||
for i := 0; i < robotUserCount; i++ {
|
||||
ids = append(ids, robotUserStart+int64(i))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func containsRoom(items []robotRoom, roomID string) bool {
|
||||
for _, item := range items {
|
||||
if item.RoomID == roomID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func env(key string, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func fail(err error) {
|
||||
fmt.Fprintln(os.Stderr, "FAIL:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user