修复
This commit is contained in:
parent
70d1912054
commit
93395f01d0
@ -145,7 +145,9 @@ type LuckyGiftDrawn struct {
|
|||||||
DrawCreatedAtMs int64 `protobuf:"varint,26,opt,name=draw_created_at_ms,json=drawCreatedAtMs,proto3" json:"draw_created_at_ms,omitempty"`
|
DrawCreatedAtMs int64 `protobuf:"varint,26,opt,name=draw_created_at_ms,json=drawCreatedAtMs,proto3" json:"draw_created_at_ms,omitempty"`
|
||||||
RewardGrantedAtMs int64 `protobuf:"varint,27,opt,name=reward_granted_at_ms,json=rewardGrantedAtMs,proto3" json:"reward_granted_at_ms,omitempty"`
|
RewardGrantedAtMs int64 `protobuf:"varint,27,opt,name=reward_granted_at_ms,json=rewardGrantedAtMs,proto3" json:"reward_granted_at_ms,omitempty"`
|
||||||
// room_locked 是 room-service 在原始送礼命令内固化的 Room Cell 锁状态;下游不能异步回查当前房间再改写历史事实。
|
// room_locked 是 room-service 在原始送礼命令内固化的 Room Cell 锁状态;下游不能异步回查当前房间再改写历史事实。
|
||||||
RoomLocked bool `protobuf:"varint,28,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
RoomLocked bool `protobuf:"varint,28,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
||||||
|
// owner_user_id 是原始送礼命令内固化的房主快照,只用于跨房展示交互,不参与抽奖或返奖。
|
||||||
|
OwnerUserId int64 `protobuf:"varint,29,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -376,6 +378,13 @@ func (x *LuckyGiftDrawn) GetRoomLocked() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *LuckyGiftDrawn) GetOwnerUserId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.OwnerUserId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
var File_proto_events_luckygift_v1_events_proto protoreflect.FileDescriptor
|
var File_proto_events_luckygift_v1_events_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
const file_proto_events_luckygift_v1_events_proto_rawDesc = "" +
|
const file_proto_events_luckygift_v1_events_proto_rawDesc = "" +
|
||||||
@ -388,7 +397,7 @@ const file_proto_events_luckygift_v1_events_proto_rawDesc = "" +
|
|||||||
"\bapp_code\x18\x03 \x01(\tR\aappCode\x12\x17\n" +
|
"\bapp_code\x18\x03 \x01(\tR\aappCode\x12\x17\n" +
|
||||||
"\adraw_id\x18\x04 \x01(\tR\x06drawId\x12$\n" +
|
"\adraw_id\x18\x04 \x01(\tR\x06drawId\x12$\n" +
|
||||||
"\x0eoccurred_at_ms\x18\x05 \x01(\x03R\foccurredAtMs\x12\x12\n" +
|
"\x0eoccurred_at_ms\x18\x05 \x01(\x03R\foccurredAtMs\x12\x12\n" +
|
||||||
"\x04body\x18\x06 \x01(\fR\x04body\"\xcc\b\n" +
|
"\x04body\x18\x06 \x01(\fR\x04body\"\xf0\b\n" +
|
||||||
"\x0eLuckyGiftDrawn\x12\x17\n" +
|
"\x0eLuckyGiftDrawn\x12\x17\n" +
|
||||||
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
@ -424,7 +433,8 @@ const file_proto_events_luckygift_v1_events_proto_rawDesc = "" +
|
|||||||
"\x12draw_created_at_ms\x18\x1a \x01(\x03R\x0fdrawCreatedAtMs\x12/\n" +
|
"\x12draw_created_at_ms\x18\x1a \x01(\x03R\x0fdrawCreatedAtMs\x12/\n" +
|
||||||
"\x14reward_granted_at_ms\x18\x1b \x01(\x03R\x11rewardGrantedAtMs\x12\x1f\n" +
|
"\x14reward_granted_at_ms\x18\x1b \x01(\x03R\x11rewardGrantedAtMs\x12\x1f\n" +
|
||||||
"\vroom_locked\x18\x1c \x01(\bR\n" +
|
"\vroom_locked\x18\x1c \x01(\bR\n" +
|
||||||
"roomLockedB=Z;hyapp.local/api/proto/events/luckygift/v1;luckygifteventsv1b\x06proto3"
|
"roomLocked\x12\"\n" +
|
||||||
|
"\rowner_user_id\x18\x1d \x01(\x03R\vownerUserIdB=Z;hyapp.local/api/proto/events/luckygift/v1;luckygifteventsv1b\x06proto3"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
file_proto_events_luckygift_v1_events_proto_rawDescOnce sync.Once
|
file_proto_events_luckygift_v1_events_proto_rawDescOnce sync.Once
|
||||||
|
|||||||
@ -53,4 +53,6 @@ message LuckyGiftDrawn {
|
|||||||
int64 reward_granted_at_ms = 27;
|
int64 reward_granted_at_ms = 27;
|
||||||
// room_locked 是 room-service 在原始送礼命令内固化的 Room Cell 锁状态;下游不能异步回查当前房间再改写历史事实。
|
// room_locked 是 room-service 在原始送礼命令内固化的 Room Cell 锁状态;下游不能异步回查当前房间再改写历史事实。
|
||||||
bool room_locked = 28;
|
bool room_locked = 28;
|
||||||
|
// owner_user_id 是原始送礼命令内固化的房主快照,只用于跨房展示交互,不参与抽奖或返奖。
|
||||||
|
int64 owner_user_id = 29;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1725,8 +1725,10 @@ type RoomGiftSent struct {
|
|||||||
SenderVipLevel int32 `protobuf:"varint,33,opt,name=sender_vip_level,json=senderVipLevel,proto3" json:"sender_vip_level,omitempty"`
|
SenderVipLevel int32 `protobuf:"varint,33,opt,name=sender_vip_level,json=senderVipLevel,proto3" json:"sender_vip_level,omitempty"`
|
||||||
// sender_gift_broadcast_animation_url 是送礼入口从有效 gift_tray_skin 权益固化的动效 URL;下游禁止异步回查当前 VIP 覆盖历史事实。
|
// sender_gift_broadcast_animation_url 是送礼入口从有效 gift_tray_skin 权益固化的动效 URL;下游禁止异步回查当前 VIP 覆盖历史事实。
|
||||||
SenderGiftBroadcastAnimationUrl string `protobuf:"bytes,34,opt,name=sender_gift_broadcast_animation_url,json=senderGiftBroadcastAnimationUrl,proto3" json:"sender_gift_broadcast_animation_url,omitempty"`
|
SenderGiftBroadcastAnimationUrl string `protobuf:"bytes,34,opt,name=sender_gift_broadcast_animation_url,json=senderGiftBroadcastAnimationUrl,proto3" json:"sender_gift_broadcast_animation_url,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// owner_user_id 固化送礼命令执行时的 Room Cell 房主;跨房飘屏只能用它优化密码框,JoinRoom 仍做最终权限校验。
|
||||||
sizeCache protoimpl.SizeCache
|
OwnerUserId int64 `protobuf:"varint,35,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *RoomGiftSent) Reset() {
|
func (x *RoomGiftSent) Reset() {
|
||||||
@ -1997,6 +1999,13 @@ func (x *RoomGiftSent) GetSenderGiftBroadcastAnimationUrl() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *RoomGiftSent) GetOwnerUserId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.OwnerUserId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// RoomGiftBatchLuckyResult 是批量送礼展示事件内的逐目标幸运礼物结果快照。
|
// RoomGiftBatchLuckyResult 是批量送礼展示事件内的逐目标幸运礼物结果快照。
|
||||||
// 它只进入房间 IM 展示,不作为 activity 中奖事实,也不携带送礼人余额。
|
// 它只进入房间 IM 展示,不作为 activity 中奖事实,也不携带送礼人余额。
|
||||||
type RoomGiftBatchLuckyResult struct {
|
type RoomGiftBatchLuckyResult struct {
|
||||||
@ -2553,7 +2562,9 @@ type RoomLuckyGiftDrawn struct {
|
|||||||
DrawCreatedAtMs int64 `protobuf:"varint,25,opt,name=draw_created_at_ms,json=drawCreatedAtMs,proto3" json:"draw_created_at_ms,omitempty"`
|
DrawCreatedAtMs int64 `protobuf:"varint,25,opt,name=draw_created_at_ms,json=drawCreatedAtMs,proto3" json:"draw_created_at_ms,omitempty"`
|
||||||
RewardGrantedAtMs int64 `protobuf:"varint,26,opt,name=reward_granted_at_ms,json=rewardGrantedAtMs,proto3" json:"reward_granted_at_ms,omitempty"`
|
RewardGrantedAtMs int64 `protobuf:"varint,26,opt,name=reward_granted_at_ms,json=rewardGrantedAtMs,proto3" json:"reward_granted_at_ms,omitempty"`
|
||||||
// room_locked 来自 lucky owner 事实中的送礼时房间快照,仅用于客户端点击飘屏前决定是否先收集密码。
|
// room_locked 来自 lucky owner 事实中的送礼时房间快照,仅用于客户端点击飘屏前决定是否先收集密码。
|
||||||
RoomLocked bool `protobuf:"varint,27,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
RoomLocked bool `protobuf:"varint,27,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
||||||
|
// owner_user_id 与 room_locked 来自同一次原始送礼快照,禁止用中奖人、送礼人或异步房间查询替代。
|
||||||
|
OwnerUserId int64 `protobuf:"varint,28,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -2777,6 +2788,13 @@ func (x *RoomLuckyGiftDrawn) GetRoomLocked() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *RoomLuckyGiftDrawn) GetOwnerUserId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.OwnerUserId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// RoomRobotLuckyGiftDrawn 是机器人房间专用幸运礼物展示事件。
|
// RoomRobotLuckyGiftDrawn 是机器人房间专用幸运礼物展示事件。
|
||||||
// 它复用客户端 lucky_gift_drawn 表现协议,但不代表真实奖池抽奖和钱包返奖事实。
|
// 它复用客户端 lucky_gift_drawn 表现协议,但不代表真实奖池抽奖和钱包返奖事实。
|
||||||
type RoomRobotLuckyGiftDrawn struct {
|
type RoomRobotLuckyGiftDrawn struct {
|
||||||
@ -3308,7 +3326,9 @@ type RoomRocketIgnited struct {
|
|||||||
RocketCoverUrl string `protobuf:"bytes,13,opt,name=rocket_cover_url,json=rocketCoverUrl,proto3" json:"rocket_cover_url,omitempty"`
|
RocketCoverUrl string `protobuf:"bytes,13,opt,name=rocket_cover_url,json=rocketCoverUrl,proto3" json:"rocket_cover_url,omitempty"`
|
||||||
ResetAtMs int64 `protobuf:"varint,14,opt,name=reset_at_ms,json=resetAtMs,proto3" json:"reset_at_ms,omitempty"`
|
ResetAtMs int64 `protobuf:"varint,14,opt,name=reset_at_ms,json=resetAtMs,proto3" json:"reset_at_ms,omitempty"`
|
||||||
// room_locked 固化点火礼物在 Room Cell 串行执行时的锁房状态,跨房点击不能异步回查后覆盖该次展示事实。
|
// room_locked 固化点火礼物在 Room Cell 串行执行时的锁房状态,跨房点击不能异步回查后覆盖该次展示事实。
|
||||||
RoomLocked bool `protobuf:"varint,15,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
RoomLocked bool `protobuf:"varint,15,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
||||||
|
// owner_user_id 固化点火瞬间的 Room Cell 房主,火箭贡献者和点火人都不能替代该身份。
|
||||||
|
OwnerUserId int64 `protobuf:"varint,16,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -3448,6 +3468,13 @@ func (x *RoomRocketIgnited) GetRoomLocked() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *RoomRocketIgnited) GetOwnerUserId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.OwnerUserId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// RoomRocketLaunched 表达火箭已经发射,在线用户名单按发射瞬间 Room Cell presence 结算。
|
// RoomRocketLaunched 表达火箭已经发射,在线用户名单按发射瞬间 Room Cell presence 结算。
|
||||||
type RoomRocketLaunched struct {
|
type RoomRocketLaunched struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
@ -3783,7 +3810,7 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
|||||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\\\n" +
|
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\\\n" +
|
||||||
"\x10RoomUserUnbanned\x12\"\n" +
|
"\x10RoomUserUnbanned\x12\"\n" +
|
||||||
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
|
"\ractor_user_id\x18\x01 \x01(\x03R\vactorUserId\x12$\n" +
|
||||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\x8a\v\n" +
|
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\"\xae\v\n" +
|
||||||
"\fRoomGiftSent\x12$\n" +
|
"\fRoomGiftSent\x12$\n" +
|
||||||
"\x0esender_user_id\x18\x01 \x01(\x03R\fsenderUserId\x12$\n" +
|
"\x0esender_user_id\x18\x01 \x01(\x03R\fsenderUserId\x12$\n" +
|
||||||
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x17\n" +
|
"\x0etarget_user_id\x18\x02 \x01(\x03R\ftargetUserId\x12\x17\n" +
|
||||||
@ -3826,7 +3853,8 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
|||||||
"\vroom_locked\x18 \x01(\bR\n" +
|
"\vroom_locked\x18 \x01(\bR\n" +
|
||||||
"roomLocked\x12(\n" +
|
"roomLocked\x12(\n" +
|
||||||
"\x10sender_vip_level\x18! \x01(\x05R\x0esenderVipLevel\x12L\n" +
|
"\x10sender_vip_level\x18! \x01(\x05R\x0esenderVipLevel\x12L\n" +
|
||||||
"#sender_gift_broadcast_animation_url\x18\" \x01(\tR\x1fsenderGiftBroadcastAnimationUrl\"\xdc\x04\n" +
|
"#sender_gift_broadcast_animation_url\x18\" \x01(\tR\x1fsenderGiftBroadcastAnimationUrl\x12\"\n" +
|
||||||
|
"\rowner_user_id\x18# \x01(\x03R\vownerUserId\"\xdc\x04\n" +
|
||||||
"\x18RoomGiftBatchLuckyResult\x12\x18\n" +
|
"\x18RoomGiftBatchLuckyResult\x12\x18\n" +
|
||||||
"\aenabled\x18\x01 \x01(\bR\aenabled\x12\x17\n" +
|
"\aenabled\x18\x01 \x01(\bR\aenabled\x12\x17\n" +
|
||||||
"\adraw_id\x18\x02 \x01(\tR\x06drawId\x12\x1d\n" +
|
"\adraw_id\x18\x02 \x01(\tR\x06drawId\x12\x1d\n" +
|
||||||
@ -3891,7 +3919,7 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
|||||||
"command_id\x18\x15 \x01(\tR\tcommandId\x12*\n" +
|
"command_id\x18\x15 \x01(\tR\tcommandId\x12*\n" +
|
||||||
"\x11visible_region_id\x18\x16 \x01(\x03R\x0fvisibleRegionId\x12(\n" +
|
"\x11visible_region_id\x18\x16 \x01(\x03R\x0fvisibleRegionId\x12(\n" +
|
||||||
"\x10sender_vip_level\x18\x17 \x01(\x05R\x0esenderVipLevel\x12L\n" +
|
"\x10sender_vip_level\x18\x17 \x01(\x05R\x0esenderVipLevel\x12L\n" +
|
||||||
"#sender_gift_broadcast_animation_url\x18\x18 \x01(\tR\x1fsenderGiftBroadcastAnimationUrl\"\xb7\b\n" +
|
"#sender_gift_broadcast_animation_url\x18\x18 \x01(\tR\x1fsenderGiftBroadcastAnimationUrl\"\xdb\b\n" +
|
||||||
"\x12RoomLuckyGiftDrawn\x12\x17\n" +
|
"\x12RoomLuckyGiftDrawn\x12\x17\n" +
|
||||||
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
@ -3926,7 +3954,8 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
|||||||
"\x12draw_created_at_ms\x18\x19 \x01(\x03R\x0fdrawCreatedAtMs\x12/\n" +
|
"\x12draw_created_at_ms\x18\x19 \x01(\x03R\x0fdrawCreatedAtMs\x12/\n" +
|
||||||
"\x14reward_granted_at_ms\x18\x1a \x01(\x03R\x11rewardGrantedAtMs\x12\x1f\n" +
|
"\x14reward_granted_at_ms\x18\x1a \x01(\x03R\x11rewardGrantedAtMs\x12\x1f\n" +
|
||||||
"\vroom_locked\x18\x1b \x01(\bR\n" +
|
"\vroom_locked\x18\x1b \x01(\bR\n" +
|
||||||
"roomLocked\"\x80\x04\n" +
|
"roomLocked\x12\"\n" +
|
||||||
|
"\rowner_user_id\x18\x1c \x01(\x03R\vownerUserId\"\x80\x04\n" +
|
||||||
"\x17RoomRobotLuckyGiftDrawn\x12\x17\n" +
|
"\x17RoomRobotLuckyGiftDrawn\x12\x17\n" +
|
||||||
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
"\adraw_id\x18\x01 \x01(\tR\x06drawId\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
@ -3981,7 +4010,7 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
|||||||
"gift_count\x18\f \x01(\x05R\tgiftCount\x12\x1d\n" +
|
"gift_count\x18\f \x01(\x05R\tgiftCount\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"command_id\x18\r \x01(\tR\tcommandId\x12*\n" +
|
"command_id\x18\r \x01(\tR\tcommandId\x12*\n" +
|
||||||
"\x11visible_region_id\x18\x0e \x01(\x03R\x0fvisibleRegionId\"\xa3\x04\n" +
|
"\x11visible_region_id\x18\x0e \x01(\x03R\x0fvisibleRegionId\"\xc7\x04\n" +
|
||||||
"\x11RoomRocketIgnited\x12\x1b\n" +
|
"\x11RoomRocketIgnited\x12\x1b\n" +
|
||||||
"\trocket_id\x18\x01 \x01(\tR\brocketId\x12\x14\n" +
|
"\trocket_id\x18\x01 \x01(\tR\brocketId\x12\x14\n" +
|
||||||
"\x05level\x18\x02 \x01(\x05R\x05level\x12!\n" +
|
"\x05level\x18\x02 \x01(\x05R\x05level\x12!\n" +
|
||||||
@ -4002,7 +4031,8 @@ const file_proto_events_room_v1_events_proto_rawDesc = "" +
|
|||||||
"\x10rocket_cover_url\x18\r \x01(\tR\x0erocketCoverUrl\x12\x1e\n" +
|
"\x10rocket_cover_url\x18\r \x01(\tR\x0erocketCoverUrl\x12\x1e\n" +
|
||||||
"\vreset_at_ms\x18\x0e \x01(\x03R\tresetAtMs\x12\x1f\n" +
|
"\vreset_at_ms\x18\x0e \x01(\x03R\tresetAtMs\x12\x1f\n" +
|
||||||
"\vroom_locked\x18\x0f \x01(\bR\n" +
|
"\vroom_locked\x18\x0f \x01(\bR\n" +
|
||||||
"roomLocked\"\xe6\x02\n" +
|
"roomLocked\x12\"\n" +
|
||||||
|
"\rowner_user_id\x18\x10 \x01(\x03R\vownerUserId\"\xe6\x02\n" +
|
||||||
"\x12RoomRocketLaunched\x12\x1b\n" +
|
"\x12RoomRocketLaunched\x12\x1b\n" +
|
||||||
"\trocket_id\x18\x01 \x01(\tR\brocketId\x12\x14\n" +
|
"\trocket_id\x18\x01 \x01(\tR\brocketId\x12\x14\n" +
|
||||||
"\x05level\x18\x02 \x01(\x05R\x05level\x12\x1d\n" +
|
"\x05level\x18\x02 \x01(\x05R\x05level\x12\x1d\n" +
|
||||||
|
|||||||
@ -254,6 +254,8 @@ message RoomGiftSent {
|
|||||||
int32 sender_vip_level = 33;
|
int32 sender_vip_level = 33;
|
||||||
// sender_gift_broadcast_animation_url 是送礼入口从有效 gift_tray_skin 权益固化的动效 URL;下游禁止异步回查当前 VIP 覆盖历史事实。
|
// sender_gift_broadcast_animation_url 是送礼入口从有效 gift_tray_skin 权益固化的动效 URL;下游禁止异步回查当前 VIP 覆盖历史事实。
|
||||||
string sender_gift_broadcast_animation_url = 34;
|
string sender_gift_broadcast_animation_url = 34;
|
||||||
|
// owner_user_id 固化送礼命令执行时的 Room Cell 房主;跨房飘屏只能用它优化密码框,JoinRoom 仍做最终权限校验。
|
||||||
|
int64 owner_user_id = 35;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomGiftBatchLuckyResult 是批量送礼展示事件内的逐目标幸运礼物结果快照。
|
// RoomGiftBatchLuckyResult 是批量送礼展示事件内的逐目标幸运礼物结果快照。
|
||||||
@ -356,6 +358,8 @@ message RoomLuckyGiftDrawn {
|
|||||||
int64 reward_granted_at_ms = 26;
|
int64 reward_granted_at_ms = 26;
|
||||||
// room_locked 来自 lucky owner 事实中的送礼时房间快照,仅用于客户端点击飘屏前决定是否先收集密码。
|
// room_locked 来自 lucky owner 事实中的送礼时房间快照,仅用于客户端点击飘屏前决定是否先收集密码。
|
||||||
bool room_locked = 27;
|
bool room_locked = 27;
|
||||||
|
// owner_user_id 与 room_locked 来自同一次原始送礼快照,禁止用中奖人、送礼人或异步房间查询替代。
|
||||||
|
int64 owner_user_id = 28;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomRobotLuckyGiftDrawn 是机器人房间专用幸运礼物展示事件。
|
// RoomRobotLuckyGiftDrawn 是机器人房间专用幸运礼物展示事件。
|
||||||
@ -437,6 +441,8 @@ message RoomRocketIgnited {
|
|||||||
int64 reset_at_ms = 14;
|
int64 reset_at_ms = 14;
|
||||||
// room_locked 固化点火礼物在 Room Cell 串行执行时的锁房状态,跨房点击不能异步回查后覆盖该次展示事实。
|
// room_locked 固化点火礼物在 Room Cell 串行执行时的锁房状态,跨房点击不能异步回查后覆盖该次展示事实。
|
||||||
bool room_locked = 15;
|
bool room_locked = 15;
|
||||||
|
// owner_user_id 固化点火瞬间的 Room Cell 房主,火箭贡献者和点火人都不能替代该身份。
|
||||||
|
int64 owner_user_id = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomRocketLaunched 表达火箭已经发射,在线用户名单按发射瞬间 Room Cell presence 结算。
|
// RoomRocketLaunched 表达火箭已经发射,在线用户名单按发射瞬间 Room Cell presence 结算。
|
||||||
|
|||||||
@ -134,7 +134,9 @@ type LuckyGiftMeta struct {
|
|||||||
// dynamic_v3 只用它判断闭窗时账号是否已满 48 小时;缺失时普通开奖继续,但补偿大奖资格 fail-close。
|
// dynamic_v3 只用它判断闭窗时账号是否已满 48 小时;缺失时普通开奖继续,但补偿大奖资格 fail-close。
|
||||||
UserRegisteredAtMs int64 `protobuf:"varint,23,opt,name=user_registered_at_ms,json=userRegisteredAtMs,proto3" json:"user_registered_at_ms,omitempty"`
|
UserRegisteredAtMs int64 `protobuf:"varint,23,opt,name=user_registered_at_ms,json=userRegisteredAtMs,proto3" json:"user_registered_at_ms,omitempty"`
|
||||||
// room_locked 由 Room Cell 在送礼命令串行执行期间读取,只进入中奖展示事实,不参与抽奖或账务计算。
|
// room_locked 由 Room Cell 在送礼命令串行执行期间读取,只进入中奖展示事实,不参与抽奖或账务计算。
|
||||||
RoomLocked bool `protobuf:"varint,24,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
RoomLocked bool `protobuf:"varint,24,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
||||||
|
// owner_user_id 与 room_locked 在同一 Room Cell 串行区间固化;lucky owner 只持久化并透传该展示快照。
|
||||||
|
OwnerUserId int64 `protobuf:"varint,25,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -337,6 +339,13 @@ func (x *LuckyGiftMeta) GetRoomLocked() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *LuckyGiftMeta) GetOwnerUserId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.OwnerUserId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type LuckyGiftRuleTier struct {
|
type LuckyGiftRuleTier struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Stage string `protobuf:"bytes,1,opt,name=stage,proto3" json:"stage,omitempty"`
|
Stage string `protobuf:"bytes,1,opt,name=stage,proto3" json:"stage,omitempty"`
|
||||||
@ -5646,7 +5655,7 @@ const file_proto_luckygift_v1_luckygift_proto_rawDesc = "" +
|
|||||||
"\x0fgateway_node_id\x18\x03 \x01(\tR\rgatewayNodeId\x12\x1c\n" +
|
"\x0fgateway_node_id\x18\x03 \x01(\tR\rgatewayNodeId\x12\x1c\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"sent_at_ms\x18\x04 \x01(\x03R\bsentAtMs\x12\x19\n" +
|
"sent_at_ms\x18\x04 \x01(\x03R\bsentAtMs\x12\x19\n" +
|
||||||
"\bapp_code\x18\x05 \x01(\tR\aappCode\"\x96\a\n" +
|
"\bapp_code\x18\x05 \x01(\tR\aappCode\"\xba\a\n" +
|
||||||
"\rLuckyGiftMeta\x123\n" +
|
"\rLuckyGiftMeta\x123\n" +
|
||||||
"\x04meta\x18\x01 \x01(\v2\x1f.hyapp.luckygift.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
"\x04meta\x18\x01 \x01(\v2\x1f.hyapp.luckygift.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
@ -5679,7 +5688,8 @@ const file_proto_luckygift_v1_luckygift_proto_rawDesc = "" +
|
|||||||
"\x11gift_income_coins\x18\x16 \x01(\x03R\x0fgiftIncomeCoins\x121\n" +
|
"\x11gift_income_coins\x18\x16 \x01(\x03R\x0fgiftIncomeCoins\x121\n" +
|
||||||
"\x15user_registered_at_ms\x18\x17 \x01(\x03R\x12userRegisteredAtMs\x12\x1f\n" +
|
"\x15user_registered_at_ms\x18\x17 \x01(\x03R\x12userRegisteredAtMs\x12\x1f\n" +
|
||||||
"\vroom_locked\x18\x18 \x01(\bR\n" +
|
"\vroom_locked\x18\x18 \x01(\bR\n" +
|
||||||
"roomLocked\"\xd3\x01\n" +
|
"roomLocked\x12\"\n" +
|
||||||
|
"\rowner_user_id\x18\x19 \x01(\x03R\vownerUserId\"\xd3\x01\n" +
|
||||||
"\x11LuckyGiftRuleTier\x12\x14\n" +
|
"\x11LuckyGiftRuleTier\x12\x14\n" +
|
||||||
"\x05stage\x18\x01 \x01(\tR\x05stage\x12\x17\n" +
|
"\x05stage\x18\x01 \x01(\tR\x05stage\x12\x17\n" +
|
||||||
"\atier_id\x18\x02 \x01(\tR\x06tierId\x12%\n" +
|
"\atier_id\x18\x02 \x01(\tR\x06tierId\x12%\n" +
|
||||||
|
|||||||
@ -49,6 +49,8 @@ message LuckyGiftMeta {
|
|||||||
int64 user_registered_at_ms = 23;
|
int64 user_registered_at_ms = 23;
|
||||||
// room_locked 由 Room Cell 在送礼命令串行执行期间读取,只进入中奖展示事实,不参与抽奖或账务计算。
|
// room_locked 由 Room Cell 在送礼命令串行执行期间读取,只进入中奖展示事实,不参与抽奖或账务计算。
|
||||||
bool room_locked = 24;
|
bool room_locked = 24;
|
||||||
|
// owner_user_id 与 room_locked 在同一 Room Cell 串行区间固化;lucky owner 只持久化并透传该展示快照。
|
||||||
|
int64 owner_user_id = 25;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LuckyGiftRuleTier {
|
message LuckyGiftRuleTier {
|
||||||
|
|||||||
@ -24067,7 +24067,9 @@ type CreateRedPacketRequest struct {
|
|||||||
TotalAmount int64 `protobuf:"varint,8,opt,name=total_amount,json=totalAmount,proto3" json:"total_amount,omitempty"`
|
TotalAmount int64 `protobuf:"varint,8,opt,name=total_amount,json=totalAmount,proto3" json:"total_amount,omitempty"`
|
||||||
PacketCount int32 `protobuf:"varint,9,opt,name=packet_count,json=packetCount,proto3" json:"packet_count,omitempty"`
|
PacketCount int32 `protobuf:"varint,9,opt,name=packet_count,json=packetCount,proto3" json:"packet_count,omitempty"`
|
||||||
// room_locked 由 gateway 从本次发红包前读取的 room-service snapshot 固化;wallet 只随资金事实透传。
|
// room_locked 由 gateway 从本次发红包前读取的 room-service snapshot 固化;wallet 只随资金事实透传。
|
||||||
RoomLocked bool `protobuf:"varint,10,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
RoomLocked bool `protobuf:"varint,10,opt,name=room_locked,json=roomLocked,proto3" json:"room_locked,omitempty"`
|
||||||
|
// owner_user_id 与 room_locked 来自同一份 room snapshot,只用于红包跨房入口的密码框交互。
|
||||||
|
OwnerUserId int64 `protobuf:"varint,11,opt,name=owner_user_id,json=ownerUserId,proto3" json:"owner_user_id,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@ -24172,6 +24174,13 @@ func (x *CreateRedPacketRequest) GetRoomLocked() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *CreateRedPacketRequest) GetOwnerUserId() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.OwnerUserId
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
type CreateRedPacketResponse struct {
|
type CreateRedPacketResponse struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Packet *RedPacket `protobuf:"bytes,1,opt,name=packet,proto3" json:"packet,omitempty"`
|
Packet *RedPacket `protobuf:"bytes,1,opt,name=packet,proto3" json:"packet,omitempty"`
|
||||||
@ -29674,7 +29683,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
" \x01(\tR\aruleUrl\"\x7f\n" +
|
" \x01(\tR\aruleUrl\"\x7f\n" +
|
||||||
"\x1dUpdateRedPacketConfigResponse\x128\n" +
|
"\x1dUpdateRedPacketConfigResponse\x128\n" +
|
||||||
"\x06config\x18\x01 \x01(\v2 .hyapp.wallet.v1.RedPacketConfigR\x06config\x12$\n" +
|
"\x06config\x18\x01 \x01(\v2 .hyapp.wallet.v1.RedPacketConfigR\x06config\x12$\n" +
|
||||||
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xd5\x02\n" +
|
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xf9\x02\n" +
|
||||||
"\x16CreateRedPacketRequest\x12\x1d\n" +
|
"\x16CreateRedPacketRequest\x12\x1d\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
|
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
|
||||||
@ -29690,7 +29699,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
|
|||||||
"\fpacket_count\x18\t \x01(\x05R\vpacketCount\x12\x1f\n" +
|
"\fpacket_count\x18\t \x01(\x05R\vpacketCount\x12\x1f\n" +
|
||||||
"\vroom_locked\x18\n" +
|
"\vroom_locked\x18\n" +
|
||||||
" \x01(\bR\n" +
|
" \x01(\bR\n" +
|
||||||
"roomLocked\"\x98\x01\n" +
|
"roomLocked\x12\"\n" +
|
||||||
|
"\rowner_user_id\x18\v \x01(\x03R\vownerUserId\"\x98\x01\n" +
|
||||||
"\x17CreateRedPacketResponse\x122\n" +
|
"\x17CreateRedPacketResponse\x122\n" +
|
||||||
"\x06packet\x18\x01 \x01(\v2\x1a.hyapp.wallet.v1.RedPacketR\x06packet\x12#\n" +
|
"\x06packet\x18\x01 \x01(\v2\x1a.hyapp.wallet.v1.RedPacketR\x06packet\x12#\n" +
|
||||||
"\rbalance_after\x18\x02 \x01(\x03R\fbalanceAfter\x12$\n" +
|
"\rbalance_after\x18\x02 \x01(\x03R\fbalanceAfter\x12$\n" +
|
||||||
|
|||||||
@ -2807,6 +2807,8 @@ message CreateRedPacketRequest {
|
|||||||
int32 packet_count = 9;
|
int32 packet_count = 9;
|
||||||
// room_locked 由 gateway 从本次发红包前读取的 room-service snapshot 固化;wallet 只随资金事实透传。
|
// room_locked 由 gateway 从本次发红包前读取的 room-service snapshot 固化;wallet 只随资金事实透传。
|
||||||
bool room_locked = 10;
|
bool room_locked = 10;
|
||||||
|
// owner_user_id 与 room_locked 来自同一份 room snapshot,只用于红包跨房入口的密码框交互。
|
||||||
|
int64 owner_user_id = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
message CreateRedPacketResponse {
|
message CreateRedPacketResponse {
|
||||||
|
|||||||
@ -20,6 +20,7 @@
|
|||||||
| `start_ms` / `end_ms` | 查询时间 | 本次查询的毫秒时间范围。 |
|
| `start_ms` / `end_ms` | 查询时间 | 本次查询的毫秒时间范围。 |
|
||||||
| `country_id` / `region_id` | 国家/区域 | 聚合维度;未筛选国家时返回国家分解。 |
|
| `country_id` / `region_id` | 国家/区域 | 聚合维度;未筛选国家时返回国家分解。 |
|
||||||
| `new_users` | 新增用户 | `UserRegistered` 注册 cohort 去重用户数。 |
|
| `new_users` | 新增用户 | `UserRegistered` 注册 cohort 去重用户数。 |
|
||||||
|
| `profile_setup_view_users` | 填写资料页曝光人数 | Flutter 在未完成资料的新用户真正进入 `profile_setup` 页面后上报 `profile_start`;Social 用户日读模型按 canonical 用户或设备主体、统计自然日去重。 |
|
||||||
| `active_users` | 活跃用户 | `stat_social_user_day.active_user` 经设备日级 canonical 身份归并后的用户日去重数;多日汇总为活跃人天。 |
|
| `active_users` | 活跃用户 | `stat_social_user_day.active_user` 经设备日级 canonical 身份归并后的用户日去重数;多日汇总为活跃人天。 |
|
||||||
| `paid_users` | 付费用户 | `WalletRechargeRecorded` 写入 `stat_recharge_day_payers` 后按用户去重。 |
|
| `paid_users` | 付费用户 | `WalletRechargeRecorded` 写入 `stat_recharge_day_payers` 后按用户去重。 |
|
||||||
| `new_paid_users` | 新增付费用户 | 付费用户中注册 cohort 也落在查询周期内的用户数。 |
|
| `new_paid_users` | 新增付费用户 | 付费用户中注册 cohort 也落在查询周期内的用户数。 |
|
||||||
|
|||||||
@ -27,6 +27,7 @@ var catalog = map[Code]Spec{
|
|||||||
NotFound: spec(codes.NotFound, httpStatusNotFound, NotFound, "not found"),
|
NotFound: spec(codes.NotFound, httpStatusNotFound, NotFound, "not found"),
|
||||||
Conflict: spec(codes.FailedPrecondition, httpStatusConflict, Conflict, "request cannot be completed in current state"),
|
Conflict: spec(codes.FailedPrecondition, httpStatusConflict, Conflict, "request cannot be completed in current state"),
|
||||||
RoomClosed: spec(codes.FailedPrecondition, httpStatusConflict, RoomClosed, "room closed"),
|
RoomClosed: spec(codes.FailedPrecondition, httpStatusConflict, RoomClosed, "room closed"),
|
||||||
|
RoomUserMuted: spec(codes.PermissionDenied, httpStatusForbidden, RoomUserMuted, "You have been muted and cannot join the mic."),
|
||||||
RoomRPSChallengeTaken: spec(
|
RoomRPSChallengeTaken: spec(
|
||||||
codes.FailedPrecondition,
|
codes.FailedPrecondition,
|
||||||
httpStatusConflict,
|
httpStatusConflict,
|
||||||
|
|||||||
@ -19,6 +19,8 @@ const (
|
|||||||
Conflict Code = "CONFLICT"
|
Conflict Code = "CONFLICT"
|
||||||
// RoomClosed 表示房间已被后台关闭,客户端不能再进入。
|
// RoomClosed 表示房间已被后台关闭,客户端不能再进入。
|
||||||
RoomClosed Code = "ROOM_CLOSED"
|
RoomClosed Code = "ROOM_CLOSED"
|
||||||
|
// RoomUserMuted 表示房间禁言正在阻止用户上麦、发流或重新打开麦克风。
|
||||||
|
RoomUserMuted Code = "ROOM_USER_MUTED"
|
||||||
// RoomRPSChallengeTaken 表示房内猜拳挑战已被其他用户接单,当前用户不能再重复应战。
|
// RoomRPSChallengeTaken 表示房内猜拳挑战已被其他用户接单,当前用户不能再重复应战。
|
||||||
RoomRPSChallengeTaken Code = "ROOM_RPS_CHALLENGE_TAKEN"
|
RoomRPSChallengeTaken Code = "ROOM_RPS_CHALLENGE_TAKEN"
|
||||||
// RoomRPSChallengeExpired 表示房内猜拳挑战已越过无人应战截止时间,客户端应关闭应战入口并提示过期。
|
// RoomRPSChallengeExpired 表示房内猜拳挑战已越过无人应战截止时间,客户端应关闭应战入口并提示过期。
|
||||||
|
|||||||
@ -167,6 +167,14 @@ func TestCatalogMappings(t *testing.T) {
|
|||||||
publicCode: string(CountryChangeIdentityLocked),
|
publicCode: string(CountryChangeIdentityLocked),
|
||||||
publicMessage: "You already have a host identity. Please contact customer service to change your country.",
|
publicMessage: "You already have a host identity. Please contact customer service to change your country.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "room mute tells user why mic up is blocked",
|
||||||
|
code: RoomUserMuted,
|
||||||
|
grpcCode: codes.PermissionDenied,
|
||||||
|
httpStatus: httpStatusForbidden,
|
||||||
|
publicCode: string(RoomUserMuted),
|
||||||
|
publicMessage: "You have been muted and cannot join the mic.",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "registered device conflict exposes concrete login reason",
|
name: "registered device conflict exposes concrete login reason",
|
||||||
code: DeviceAlreadyRegistered,
|
code: DeviceAlreadyRegistered,
|
||||||
|
|||||||
@ -50,6 +50,7 @@ INCREMENTAL_MIGRATION_FILES=(
|
|||||||
"services/wallet-service/deploy/mysql/migrations/013_wallet_outbox_archive_purge_receipts.sql"
|
"services/wallet-service/deploy/mysql/migrations/013_wallet_outbox_archive_purge_receipts.sql"
|
||||||
"services/wallet-service/deploy/mysql/migrations/018_host_salary_policy_revisions.sql"
|
"services/wallet-service/deploy/mysql/migrations/018_host_salary_policy_revisions.sql"
|
||||||
"services/lucky-gift-service/deploy/mysql/migrations/013_jackpot_token_version_rate_limit.sql"
|
"services/lucky-gift-service/deploy/mysql/migrations/013_jackpot_token_version_rate_limit.sql"
|
||||||
|
"services/statistics-service/deploy/mysql/migrations/003_social_profile_setup_view.sql"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Keep MySQL as the only required dependency for this script. Business services
|
# Keep MySQL as the only required dependency for this script. Business services
|
||||||
|
|||||||
@ -21,6 +21,7 @@ func TestRedPacketEventFromWalletMessageKeepsDisplayPayloadFacts(t *testing.T) {
|
|||||||
"room_id":"9001",
|
"room_id":"9001",
|
||||||
"region_id":310,
|
"region_id":310,
|
||||||
"room_locked":true,
|
"room_locked":true,
|
||||||
|
"owner_user_id":323411852102995968,
|
||||||
"total_amount":15000,
|
"total_amount":15000,
|
||||||
"packet_count":5,
|
"packet_count":5,
|
||||||
"remaining_amount":15000,
|
"remaining_amount":15000,
|
||||||
@ -39,7 +40,7 @@ func TestRedPacketEventFromWalletMessageKeepsDisplayPayloadFacts(t *testing.T) {
|
|||||||
if err != nil || !ok {
|
if err != nil || !ok {
|
||||||
t.Fatalf("decode red packet event failed: ok=%t err=%v", ok, err)
|
t.Fatalf("decode red packet event failed: ok=%t err=%v", ok, err)
|
||||||
}
|
}
|
||||||
if event.PacketID != "VRRP2026052712340001" || event.PacketType != "delayed" || event.RoomID != "9001" || event.RegionID != 310 || !event.RoomLocked {
|
if event.PacketID != "VRRP2026052712340001" || event.PacketType != "delayed" || event.RoomID != "9001" || event.RegionID != 310 || !event.RoomLocked || event.OwnerUserID != 323411852102995968 {
|
||||||
t.Fatalf("red packet identity fields mismatch: %+v", event)
|
t.Fatalf("red packet identity fields mismatch: %+v", event)
|
||||||
}
|
}
|
||||||
if event.SenderUserID != 10001 || event.TotalAmount != 15000 || event.TotalCount != 5 || event.RemainingAmount != 15000 || event.RemainingCount != 5 {
|
if event.SenderUserID != 10001 || event.TotalAmount != 15000 || event.TotalCount != 5 || event.RemainingAmount != 15000 || event.RemainingCount != 5 {
|
||||||
|
|||||||
@ -123,6 +123,7 @@ func redPacketEventFromWalletMessage(body []byte) (broadcastdomain.RedPacketWall
|
|||||||
RegionID: fields.RegionID,
|
RegionID: fields.RegionID,
|
||||||
RegionCode: fields.RegionCode,
|
RegionCode: fields.RegionCode,
|
||||||
RoomLocked: fields.RoomLocked,
|
RoomLocked: fields.RoomLocked,
|
||||||
|
OwnerUserID: fields.OwnerUserID,
|
||||||
SenderUserID: fields.SenderUserID,
|
SenderUserID: fields.SenderUserID,
|
||||||
TotalAmount: fields.TotalAmount,
|
TotalAmount: fields.TotalAmount,
|
||||||
TotalCount: fields.TotalCount,
|
TotalCount: fields.TotalCount,
|
||||||
@ -291,6 +292,7 @@ type redPacketPayloadFields struct {
|
|||||||
RegionID int64
|
RegionID int64
|
||||||
RegionCode string
|
RegionCode string
|
||||||
RoomLocked bool
|
RoomLocked bool
|
||||||
|
OwnerUserID int64
|
||||||
SenderUserID int64
|
SenderUserID int64
|
||||||
TotalAmount int64
|
TotalAmount int64
|
||||||
TotalCount int32
|
TotalCount int32
|
||||||
@ -304,7 +306,10 @@ type redPacketPayloadFields struct {
|
|||||||
|
|
||||||
func redPacketFieldsFromWalletPayload(payload string, eventType string) redPacketPayloadFields {
|
func redPacketFieldsFromWalletPayload(payload string, eventType string) redPacketPayloadFields {
|
||||||
var decoded map[string]any
|
var decoded map[string]any
|
||||||
if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
|
decoder := json.NewDecoder(strings.NewReader(payload))
|
||||||
|
// wallet payload 中的用户 ID 可能是 Snowflake int64;UseNumber 必须在首次解码时启用,先落成 float64 后无法恢复精度。
|
||||||
|
decoder.UseNumber()
|
||||||
|
if err := decoder.Decode(&decoded); err != nil {
|
||||||
return redPacketPayloadFields{}
|
return redPacketPayloadFields{}
|
||||||
}
|
}
|
||||||
packetID := stringFromDecoded(decoded, "packet_id")
|
packetID := stringFromDecoded(decoded, "packet_id")
|
||||||
@ -332,6 +337,7 @@ func redPacketFieldsFromWalletPayload(payload string, eventType string) redPacke
|
|||||||
RegionID: regionID,
|
RegionID: regionID,
|
||||||
RegionCode: stringFromDecoded(decoded, "region_code"),
|
RegionCode: stringFromDecoded(decoded, "region_code"),
|
||||||
RoomLocked: boolFromDecoded(decoded, "room_locked"),
|
RoomLocked: boolFromDecoded(decoded, "room_locked"),
|
||||||
|
OwnerUserID: int64FromDecoded(decoded, "owner_user_id"),
|
||||||
SenderUserID: int64FromDecoded(decoded, "sender_user_id"),
|
SenderUserID: int64FromDecoded(decoded, "sender_user_id"),
|
||||||
TotalAmount: int64FromDecoded(decoded, "total_amount"),
|
TotalAmount: int64FromDecoded(decoded, "total_amount"),
|
||||||
TotalCount: totalCount,
|
TotalCount: totalCount,
|
||||||
|
|||||||
@ -42,6 +42,7 @@ func (s *GRPCRoomBroadcastSource) GetRoomBroadcastSnapshot(ctx context.Context,
|
|||||||
RoomID: room.GetRoomId(),
|
RoomID: room.GetRoomId(),
|
||||||
Title: room.GetTitle(),
|
Title: room.GetTitle(),
|
||||||
Locked: room.GetLocked(),
|
Locked: room.GetLocked(),
|
||||||
|
OwnerUserID: room.GetOwnerUserId(),
|
||||||
VisibleRegionID: room.GetVisibleRegionId(),
|
VisibleRegionID: room.GetVisibleRegionId(),
|
||||||
Status: room.GetStatus(),
|
Status: room.GetStatus(),
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@ -127,6 +127,7 @@ type RedPacketWalletEvent struct {
|
|||||||
RegionID int64
|
RegionID int64
|
||||||
RegionCode string
|
RegionCode string
|
||||||
RoomLocked bool
|
RoomLocked bool
|
||||||
|
OwnerUserID int64
|
||||||
SenderUserID int64
|
SenderUserID int64
|
||||||
SenderAccount string
|
SenderAccount string
|
||||||
SenderNickname string
|
SenderNickname string
|
||||||
|
|||||||
@ -216,19 +216,24 @@ func gameWinPayload(message gamemq.GameOutboxMessage, source map[string]any, eve
|
|||||||
}
|
}
|
||||||
if roomID != "" {
|
if roomID != "" {
|
||||||
payload["jump_type"] = "room_window"
|
payload["jump_type"] = "room_window"
|
||||||
payload["jump_payload"] = map[string]any{
|
jumpPayload := map[string]any{
|
||||||
"room_id": roomID,
|
"room_id": roomID,
|
||||||
"room_locked": room.Locked,
|
"room_locked": room.Locked,
|
||||||
"window": "game_window",
|
"window": "game_window",
|
||||||
"game_id": gameID,
|
"game_id": gameID,
|
||||||
}
|
}
|
||||||
payload["action"] = map[string]any{
|
action := map[string]any{
|
||||||
"type": "room_window",
|
"type": "room_window",
|
||||||
"room_id": roomID,
|
"room_id": roomID,
|
||||||
"room_locked": room.Locked,
|
"room_locked": room.Locked,
|
||||||
"window": "game_window",
|
"window": "game_window",
|
||||||
"game_id": gameID,
|
"game_id": gameID,
|
||||||
}
|
}
|
||||||
|
setRoomOwnerSnapshot(payload, room.OwnerUserID)
|
||||||
|
setRoomOwnerSnapshot(jumpPayload, room.OwnerUserID)
|
||||||
|
setRoomOwnerSnapshot(action, room.OwnerUserID)
|
||||||
|
payload["jump_payload"] = jumpPayload
|
||||||
|
payload["action"] = action
|
||||||
}
|
}
|
||||||
encoded, err := json.Marshal(payload)
|
encoded, err := json.Marshal(payload)
|
||||||
return string(encoded), err
|
return string(encoded), err
|
||||||
|
|||||||
@ -142,6 +142,11 @@ func luckyGiftBigWinPayload(envelope *luckygifteventsv1.EventEnvelope, event *lu
|
|||||||
}
|
}
|
||||||
// 字段保持拆服前 Flutter 已消费的扁平协议,并补充 granted/pretty ID 快照;不携带 gift_name,
|
// 字段保持拆服前 Flutter 已消费的扁平协议,并补充 granted/pretty ID 快照;不携带 gift_name,
|
||||||
// 客户端继续用 gift_id 命中本地多语言礼物配置,避免服务端文案覆盖客户端翻译。
|
// 客户端继续用 gift_id 命中本地多语言礼物配置,避免服务端文案覆盖客户端翻译。
|
||||||
|
action := map[string]any{
|
||||||
|
"type": "enter_room",
|
||||||
|
"room_id": event.GetRoomId(),
|
||||||
|
"room_locked": event.GetRoomLocked(),
|
||||||
|
}
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"event_type": "lucky_gift_drawn",
|
"event_type": "lucky_gift_drawn",
|
||||||
"event_id": eventID,
|
"event_id": eventID,
|
||||||
@ -179,10 +184,7 @@ func luckyGiftBigWinPayload(envelope *luckygifteventsv1.EventEnvelope, event *lu
|
|||||||
"draw_created_at_ms": createdAtMS,
|
"draw_created_at_ms": createdAtMS,
|
||||||
"reward_granted_at_ms": event.GetRewardGrantedAtMs(),
|
"reward_granted_at_ms": event.GetRewardGrantedAtMs(),
|
||||||
"sent_at_ms": sentAtMS,
|
"sent_at_ms": sentAtMS,
|
||||||
"action": map[string]any{
|
"action": action,
|
||||||
"type": "enter_room",
|
|
||||||
"room_id": event.GetRoomId(),
|
|
||||||
},
|
|
||||||
"sender": map[string]any{
|
"sender": map[string]any{
|
||||||
"user_id": sender.UserID,
|
"user_id": sender.UserID,
|
||||||
"display_user_id": sender.Account,
|
"display_user_id": sender.Account,
|
||||||
@ -191,6 +193,8 @@ func luckyGiftBigWinPayload(envelope *luckygifteventsv1.EventEnvelope, event *lu
|
|||||||
"avatar": sender.Avatar,
|
"avatar": sender.Avatar,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
setRoomOwnerSnapshot(payload, event.GetOwnerUserId())
|
||||||
|
setRoomOwnerSnapshot(action, event.GetOwnerUserId())
|
||||||
encoded, err := json.Marshal(payload)
|
encoded, err := json.Marshal(payload)
|
||||||
return string(encoded), err
|
return string(encoded), err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -117,33 +117,38 @@ func redPacketIMPayload(event broadcastdomain.RedPacketWalletEvent, eventName st
|
|||||||
if status == "" {
|
if status == "" {
|
||||||
status = "ACTIVE"
|
status = "ACTIVE"
|
||||||
}
|
}
|
||||||
|
data := map[string]any{
|
||||||
|
"event": eventName,
|
||||||
|
"scope": scope,
|
||||||
|
"packetNo": event.PacketID,
|
||||||
|
"packetId": event.PacketID,
|
||||||
|
"roomId": event.RoomID,
|
||||||
|
"regionId": event.RegionID,
|
||||||
|
"regionCode": event.RegionCode,
|
||||||
|
"room_locked": event.RoomLocked,
|
||||||
|
"packetMode": packetMode,
|
||||||
|
"senderUserId": redPacketUserIDString(event.SenderUserID),
|
||||||
|
"actualAccount": redPacketSenderAccount(event),
|
||||||
|
"userNickname": event.SenderNickname,
|
||||||
|
"userAvatar": event.SenderAvatar,
|
||||||
|
"totalAmount": event.TotalAmount,
|
||||||
|
"totalCount": event.TotalCount,
|
||||||
|
"remainAmount": event.RemainingAmount,
|
||||||
|
"remainCount": event.RemainingCount,
|
||||||
|
"status": status,
|
||||||
|
"delaySeconds": redPacketDelaySeconds(event),
|
||||||
|
"refundedAmount": event.RefundedAmount,
|
||||||
|
"claimStartTime": event.OpenAtMS,
|
||||||
|
"expireTime": event.ExpiresAtMS,
|
||||||
|
"serverTime": event.CreatedAtMS,
|
||||||
|
}
|
||||||
|
if event.OwnerUserID > 0 {
|
||||||
|
// 红包协议使用独立 data 包装;用户 ID 直接写字符串,避免任何未经过统一播报归一化的调用方暴露 JSON 数字精度问题。
|
||||||
|
data["owner_user_id"] = redPacketUserIDString(event.OwnerUserID)
|
||||||
|
}
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"type": "ROOM_RED_PACKET",
|
"type": "ROOM_RED_PACKET",
|
||||||
"data": map[string]any{
|
"data": data,
|
||||||
"event": eventName,
|
|
||||||
"scope": scope,
|
|
||||||
"packetNo": event.PacketID,
|
|
||||||
"packetId": event.PacketID,
|
|
||||||
"roomId": event.RoomID,
|
|
||||||
"regionId": event.RegionID,
|
|
||||||
"regionCode": event.RegionCode,
|
|
||||||
"room_locked": event.RoomLocked,
|
|
||||||
"packetMode": packetMode,
|
|
||||||
"senderUserId": redPacketUserIDString(event.SenderUserID),
|
|
||||||
"actualAccount": redPacketSenderAccount(event),
|
|
||||||
"userNickname": event.SenderNickname,
|
|
||||||
"userAvatar": event.SenderAvatar,
|
|
||||||
"totalAmount": event.TotalAmount,
|
|
||||||
"totalCount": event.TotalCount,
|
|
||||||
"remainAmount": event.RemainingAmount,
|
|
||||||
"remainCount": event.RemainingCount,
|
|
||||||
"status": status,
|
|
||||||
"delaySeconds": redPacketDelaySeconds(event),
|
|
||||||
"refundedAmount": event.RefundedAmount,
|
|
||||||
"claimStartTime": event.OpenAtMS,
|
|
||||||
"expireTime": event.ExpiresAtMS,
|
|
||||||
"serverTime": event.CreatedAtMS,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
encoded, err := json.Marshal(payload)
|
encoded, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -72,6 +72,7 @@ type RoomBroadcastSnapshot struct {
|
|||||||
RoomID string
|
RoomID string
|
||||||
Title string
|
Title string
|
||||||
Locked bool
|
Locked bool
|
||||||
|
OwnerUserID int64
|
||||||
VisibleRegionID int64
|
VisibleRegionID int64
|
||||||
Status string
|
Status string
|
||||||
}
|
}
|
||||||
@ -904,6 +905,11 @@ func superGiftBroadcastEventID(envelope *roomeventsv1.EventEnvelope, gift *roome
|
|||||||
|
|
||||||
func superGiftPayload(envelope *roomeventsv1.EventEnvelope, gift *roomeventsv1.RoomGiftSent, eventID string, sentAtMS int64, sender SenderProfile, receiver SenderProfile) (string, error) {
|
func superGiftPayload(envelope *roomeventsv1.EventEnvelope, gift *roomeventsv1.RoomGiftSent, eventID string, sentAtMS int64, sender SenderProfile, receiver SenderProfile) (string, error) {
|
||||||
// payload 同时保留扁平字段和 sender/receiver 对象,旧客户端继续读原字段,新客户端可直接取完整用户资料。
|
// payload 同时保留扁平字段和 sender/receiver 对象,旧客户端继续读原字段,新客户端可直接取完整用户资料。
|
||||||
|
action := map[string]any{
|
||||||
|
"type": "enter_room",
|
||||||
|
"room_id": envelope.GetRoomId(),
|
||||||
|
"room_locked": gift.GetRoomLocked(),
|
||||||
|
}
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"event_id": eventID,
|
"event_id": eventID,
|
||||||
"broadcast_type": broadcastdomain.TypeSuperGift,
|
"broadcast_type": broadcastdomain.TypeSuperGift,
|
||||||
@ -939,11 +945,10 @@ func superGiftPayload(envelope *roomeventsv1.EventEnvelope, gift *roomeventsv1.R
|
|||||||
"source_event_id": envelope.GetEventId(),
|
"source_event_id": envelope.GetEventId(),
|
||||||
"sender": broadcastProfilePayload(sender),
|
"sender": broadcastProfilePayload(sender),
|
||||||
"receiver": broadcastProfilePayload(receiver),
|
"receiver": broadcastProfilePayload(receiver),
|
||||||
"action": map[string]any{
|
"action": action,
|
||||||
"type": "enter_room",
|
|
||||||
"room_id": envelope.GetRoomId(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
setRoomOwnerSnapshot(payload, gift.GetOwnerUserId())
|
||||||
|
setRoomOwnerSnapshot(action, gift.GetOwnerUserId())
|
||||||
encoded, err := json.Marshal(payload)
|
encoded, err := json.Marshal(payload)
|
||||||
return string(encoded), err
|
return string(encoded), err
|
||||||
}
|
}
|
||||||
@ -1000,6 +1005,11 @@ func roomPasswordChangedPayload(envelope *roomeventsv1.EventEnvelope, event *roo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func roomRocketPayload(envelope *roomeventsv1.EventEnvelope, rocket *roomeventsv1.RoomRocketIgnited, eventID string, sentAtMS int64, igniter SenderProfile) (string, error) {
|
func roomRocketPayload(envelope *roomeventsv1.EventEnvelope, rocket *roomeventsv1.RoomRocketIgnited, eventID string, sentAtMS int64, igniter SenderProfile) (string, error) {
|
||||||
|
action := map[string]any{
|
||||||
|
"type": "enter_room",
|
||||||
|
"room_id": envelope.GetRoomId(),
|
||||||
|
"room_locked": rocket.GetRoomLocked(),
|
||||||
|
}
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"event_id": eventID,
|
"event_id": eventID,
|
||||||
"broadcast_type": broadcastdomain.TypeRoomRocket,
|
"broadcast_type": broadcastdomain.TypeRoomRocket,
|
||||||
@ -1027,16 +1037,23 @@ func roomRocketPayload(envelope *roomeventsv1.EventEnvelope, rocket *roomeventsv
|
|||||||
"current_fuel": rocket.GetCurrentFuel(),
|
"current_fuel": rocket.GetCurrentFuel(),
|
||||||
"fuel_threshold": rocket.GetFuelThreshold(),
|
"fuel_threshold": rocket.GetFuelThreshold(),
|
||||||
"ignited_at_ms": rocket.GetIgnitedAtMs(),
|
"ignited_at_ms": rocket.GetIgnitedAtMs(),
|
||||||
"action": map[string]any{
|
"action": action,
|
||||||
"type": "enter_room",
|
|
||||||
"room_id": envelope.GetRoomId(),
|
|
||||||
"room_locked": rocket.GetRoomLocked(),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
setRoomOwnerSnapshot(payload, rocket.GetOwnerUserId())
|
||||||
|
setRoomOwnerSnapshot(action, rocket.GetOwnerUserId())
|
||||||
encoded, err := json.Marshal(payload)
|
encoded, err := json.Marshal(payload)
|
||||||
return string(encoded), err
|
return string(encoded), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setRoomOwnerSnapshot(payload map[string]any, ownerUserID int64) {
|
||||||
|
if payload == nil || ownerUserID <= 0 {
|
||||||
|
// 旧事件没有 owner_user_id 时必须保持字段缺失,让客户端继续弹密码框,不能发送可被误判为真实身份的 0。
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 所有出站用户 ID 都使用十进制字符串,避免 JSON/JavaScript Number 截断 Snowflake int64。
|
||||||
|
payload["owner_user_id"] = strconv.FormatInt(ownerUserID, 10)
|
||||||
|
}
|
||||||
|
|
||||||
func roomRocketRewardPayload(envelope *roomeventsv1.EventEnvelope, event *roomeventsv1.RoomRocketRewardGranted, eventID string, sentAtMS int64, profiles map[int64]SenderProfile) (string, error) {
|
func roomRocketRewardPayload(envelope *roomeventsv1.EventEnvelope, event *roomeventsv1.RoomRocketRewardGranted, eventID string, sentAtMS int64, profiles map[int64]SenderProfile) (string, error) {
|
||||||
rewards := make([]map[string]any, 0, len(event.GetRewards()))
|
rewards := make([]map[string]any, 0, len(event.GetRewards()))
|
||||||
for _, reward := range event.GetRewards() {
|
for _, reward := range event.GetRewards() {
|
||||||
|
|||||||
@ -96,6 +96,7 @@ func TestHandleRoomGiftSentCreatesRegionBroadcast(t *testing.T) {
|
|||||||
GiftValue: 999,
|
GiftValue: 999,
|
||||||
VisibleRegionId: 1001,
|
VisibleRegionId: 1001,
|
||||||
RoomLocked: true,
|
RoomLocked: true,
|
||||||
|
OwnerUserId: 323411852102995968,
|
||||||
CommandId: "cmd-send-gift-1",
|
CommandId: "cmd-send-gift-1",
|
||||||
GiftName: "Rocket",
|
GiftName: "Rocket",
|
||||||
GiftIconUrl: "https://cdn.example/rocket.png",
|
GiftIconUrl: "https://cdn.example/rocket.png",
|
||||||
@ -119,9 +120,13 @@ func TestHandleRoomGiftSentCreatesRegionBroadcast(t *testing.T) {
|
|||||||
if err := json.Unmarshal([]byte(record.PayloadJSON), &payload); err != nil {
|
if err := json.Unmarshal([]byte(record.PayloadJSON), &payload); err != nil {
|
||||||
t.Fatalf("payload is not json: %v", err)
|
t.Fatalf("payload is not json: %v", err)
|
||||||
}
|
}
|
||||||
if payload["room_id"] != "room-1001" || payload["room_locked"] != true || payload["gift_id"] != "rocket" || payload["scope"] != "region" || payload["gift_icon_url"] != "https://cdn.example/rocket.png" || payload["sender_vip_level"] != float64(7) || payload["sender_gift_broadcast_animation_url"] != "https://cdn.example/vip-gift-tray.svga" {
|
if payload["room_id"] != "room-1001" || payload["room_locked"] != true || payload["owner_user_id"] != "323411852102995968" || payload["gift_id"] != "rocket" || payload["scope"] != "region" || payload["gift_icon_url"] != "https://cdn.example/rocket.png" || payload["sender_vip_level"] != float64(7) || payload["sender_gift_broadcast_animation_url"] != "https://cdn.example/vip-gift-tray.svga" {
|
||||||
t.Fatalf("payload mismatch: %+v", payload)
|
t.Fatalf("payload mismatch: %+v", payload)
|
||||||
}
|
}
|
||||||
|
action, _ := payload["action"].(map[string]any)
|
||||||
|
if action["room_locked"] != true || action["owner_user_id"] != "323411852102995968" {
|
||||||
|
t.Fatalf("gift action must carry room owner snapshot: %+v", action)
|
||||||
|
}
|
||||||
sender, _ := payload["sender"].(map[string]any)
|
sender, _ := payload["sender"].(map[string]any)
|
||||||
receiver, _ := payload["receiver"].(map[string]any)
|
receiver, _ := payload["receiver"].(map[string]any)
|
||||||
if sender["nickname"] != "Sender" || sender["avatar_frame_url"] != "https://cdn.example/sender-frame.png" || receiver["nickname"] != "Receiver" {
|
if sender["nickname"] != "Sender" || sender["avatar_frame_url"] != "https://cdn.example/sender-frame.png" || receiver["nickname"] != "Receiver" {
|
||||||
@ -158,6 +163,7 @@ func TestHandleLuckyGiftEventCreatesTenTimesRegionBroadcastIdempotently(t *testi
|
|||||||
SenderPrettyDisplayUserId: "888042",
|
SenderPrettyDisplayUserId: "888042",
|
||||||
VisibleRegionId: 210,
|
VisibleRegionId: 210,
|
||||||
RoomLocked: true,
|
RoomLocked: true,
|
||||||
|
OwnerUserId: 323411852102995968,
|
||||||
CountryId: 15,
|
CountryId: 15,
|
||||||
CoinSpent: 100,
|
CoinSpent: 100,
|
||||||
RuleVersion: 7,
|
RuleVersion: 7,
|
||||||
@ -191,14 +197,14 @@ func TestHandleLuckyGiftEventCreatesTenTimesRegionBroadcastIdempotently(t *testi
|
|||||||
if payload["event_type"] != "lucky_gift_drawn" || payload["reward_status"] != "granted" || payload["sender_name"] != "Real Sender" || payload["sender_pretty_display_user_id"] != "888042" {
|
if payload["event_type"] != "lucky_gift_drawn" || payload["reward_status"] != "granted" || payload["sender_name"] != "Real Sender" || payload["sender_pretty_display_user_id"] != "888042" {
|
||||||
t.Fatalf("lucky gift payload identity/profile mismatch: %+v", payload)
|
t.Fatalf("lucky gift payload identity/profile mismatch: %+v", payload)
|
||||||
}
|
}
|
||||||
if payload["room_id"] != "room-210" || payload["room_locked"] != true || payload["draw_id"] != "lucky_draw_10x" || payload["multiplier_ppm"] != float64(luckygiftmq.LuckyGiftDisplayMinMultiplierPPM) || payload["effective_reward_coins"] != float64(1000) || payload["sent_at_ms"] != float64(1_700_000_001_000) {
|
if payload["room_id"] != "room-210" || payload["room_locked"] != true || payload["owner_user_id"] != "323411852102995968" || payload["draw_id"] != "lucky_draw_10x" || payload["multiplier_ppm"] != float64(luckygiftmq.LuckyGiftDisplayMinMultiplierPPM) || payload["effective_reward_coins"] != float64(1000) || payload["sent_at_ms"] != float64(1_700_000_001_000) {
|
||||||
t.Fatalf("lucky gift payload reward mismatch: %+v", payload)
|
t.Fatalf("lucky gift payload reward mismatch: %+v", payload)
|
||||||
}
|
}
|
||||||
if _, exists := payload["gift_name"]; exists {
|
if _, exists := payload["gift_name"]; exists {
|
||||||
t.Fatalf("region payload must let Flutter resolve localized gift name: %+v", payload)
|
t.Fatalf("region payload must let Flutter resolve localized gift name: %+v", payload)
|
||||||
}
|
}
|
||||||
action, _ := payload["action"].(map[string]any)
|
action, _ := payload["action"].(map[string]any)
|
||||||
if action["type"] != "enter_room" || action["room_id"] != "room-210" {
|
if action["type"] != "enter_room" || action["room_id"] != "room-210" || action["room_locked"] != true || action["owner_user_id"] != "323411852102995968" {
|
||||||
t.Fatalf("lucky gift action mismatch: %+v", action)
|
t.Fatalf("lucky gift action mismatch: %+v", action)
|
||||||
}
|
}
|
||||||
sender, _ := payload["sender"].(map[string]any)
|
sender, _ := payload["sender"].(map[string]any)
|
||||||
@ -220,7 +226,7 @@ func TestHandleGameWinUsesAppThresholdAndLockedRoomSnapshot(t *testing.T) {
|
|||||||
service := New(Config{NodeID: "node-game-win", GameWinMinCoins: 100000}, repository, nil, nil)
|
service := New(Config{NodeID: "node-game-win", GameWinMinCoins: 100000}, repository, nil, nil)
|
||||||
service.SetClock(func() time.Time { return time.UnixMilli(1_800_000_000_000).UTC() })
|
service.SetClock(func() time.Time { return time.UnixMilli(1_800_000_000_000).UTC() })
|
||||||
service.SetSenderProfileSource(fakeSenderProfileSource{profile: SenderProfile{UserID: 42, Account: "160042", Nickname: "Winner", Avatar: "https://cdn.example/winner.png"}})
|
service.SetSenderProfileSource(fakeSenderProfileSource{profile: SenderProfile{UserID: 42, Account: "160042", Nickname: "Winner", Avatar: "https://cdn.example/winner.png"}})
|
||||||
service.SetRoomSnapshotSource(fakeRoomBroadcastSource{snapshot: RoomBroadcastSnapshot{RoomID: "room-310", Title: "Game Room", Locked: true, VisibleRegionID: 310, Status: "active"}})
|
service.SetRoomSnapshotSource(fakeRoomBroadcastSource{snapshot: RoomBroadcastSnapshot{RoomID: "room-310", Title: "Game Room", Locked: true, OwnerUserID: 323411852102995968, VisibleRegionID: 310, Status: "active"}})
|
||||||
ctx := appcode.WithContext(context.Background(), "fami")
|
ctx := appcode.WithContext(context.Background(), "fami")
|
||||||
config, err := service.UpdateGameWinBroadcastConfig(ctx, true, 500, 7)
|
config, err := service.UpdateGameWinBroadcastConfig(ctx, true, 500, 7)
|
||||||
if err != nil || config.MinWinCoins != 500 || config.UpdatedByAdminID != 7 {
|
if err != nil || config.MinWinCoins != 500 || config.UpdatedByAdminID != 7 {
|
||||||
@ -248,13 +254,17 @@ func TestHandleGameWinUsesAppThresholdAndLockedRoomSnapshot(t *testing.T) {
|
|||||||
if err := json.Unmarshal([]byte(record.PayloadJSON), &payload); err != nil {
|
if err := json.Unmarshal([]byte(record.PayloadJSON), &payload); err != nil {
|
||||||
t.Fatalf("decode game win payload: %v", err)
|
t.Fatalf("decode game win payload: %v", err)
|
||||||
}
|
}
|
||||||
if payload["room_id"] != "room-310" || payload["room_locked"] != true || payload["coins"] != float64(500) || payload["game_name"] != "Slots" || payload["user_name"] != "Winner" {
|
if payload["room_id"] != "room-310" || payload["room_locked"] != true || payload["owner_user_id"] != "323411852102995968" || payload["coins"] != float64(500) || payload["game_name"] != "Slots" || payload["user_name"] != "Winner" {
|
||||||
t.Fatalf("game win payload mismatch: %+v", payload)
|
t.Fatalf("game win payload mismatch: %+v", payload)
|
||||||
}
|
}
|
||||||
jump, _ := payload["jump_payload"].(map[string]any)
|
jump, _ := payload["jump_payload"].(map[string]any)
|
||||||
if jump["room_locked"] != true || jump["window"] != "game_window" {
|
if jump["room_locked"] != true || jump["owner_user_id"] != "323411852102995968" || jump["window"] != "game_window" {
|
||||||
t.Fatalf("game win jump must carry lock and game window: %+v", jump)
|
t.Fatalf("game win jump must carry lock and game window: %+v", jump)
|
||||||
}
|
}
|
||||||
|
action, _ := payload["action"].(map[string]any)
|
||||||
|
if action["owner_user_id"] != "323411852102995968" {
|
||||||
|
t.Fatalf("game win action must carry owner snapshot: %+v", action)
|
||||||
|
}
|
||||||
|
|
||||||
message.OrderID = "order-below"
|
message.OrderID = "order-below"
|
||||||
message.EventID = "GameOrderSettled:order-below"
|
message.EventID = "GameOrderSettled:order-below"
|
||||||
@ -381,6 +391,7 @@ func TestRoomRocketPayloadCarriesRoomLockSnapshot(t *testing.T) {
|
|||||||
RocketId: "rocket-1",
|
RocketId: "rocket-1",
|
||||||
BroadcastScope: broadcastdomain.ScopeGlobal,
|
BroadcastScope: broadcastdomain.ScopeGlobal,
|
||||||
RoomLocked: true,
|
RoomLocked: true,
|
||||||
|
OwnerUserId: 323411852102995968,
|
||||||
}, "rocket-event-1", 1_700_000_000_000, SenderProfile{})
|
}, "rocket-event-1", 1_700_000_000_000, SenderProfile{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("roomRocketPayload failed: %v", err)
|
t.Fatalf("roomRocketPayload failed: %v", err)
|
||||||
@ -390,7 +401,7 @@ func TestRoomRocketPayloadCarriesRoomLockSnapshot(t *testing.T) {
|
|||||||
t.Fatalf("rocket payload is not json: %v", err)
|
t.Fatalf("rocket payload is not json: %v", err)
|
||||||
}
|
}
|
||||||
action, _ := payload["action"].(map[string]any)
|
action, _ := payload["action"].(map[string]any)
|
||||||
if payload["room_locked"] != true || action["room_locked"] != true {
|
if payload["room_locked"] != true || action["room_locked"] != true || payload["owner_user_id"] != "323411852102995968" || action["owner_user_id"] != "323411852102995968" {
|
||||||
t.Fatalf("rocket payload and action must carry room lock snapshot: %+v", payload)
|
t.Fatalf("rocket payload and action must carry room lock snapshot: %+v", payload)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -511,6 +522,7 @@ func TestConsumeRedPacketCreatedBuildsFullIMPayload(t *testing.T) {
|
|||||||
RoomID: "9001",
|
RoomID: "9001",
|
||||||
RegionID: 310,
|
RegionID: 310,
|
||||||
RoomLocked: true,
|
RoomLocked: true,
|
||||||
|
OwnerUserID: 323411852102995968,
|
||||||
SenderUserID: 10001,
|
SenderUserID: 10001,
|
||||||
TotalAmount: 15000,
|
TotalAmount: 15000,
|
||||||
TotalCount: 5,
|
TotalCount: 5,
|
||||||
@ -543,6 +555,9 @@ func TestConsumeRedPacketCreatedBuildsFullIMPayload(t *testing.T) {
|
|||||||
if data["room_locked"] != true {
|
if data["room_locked"] != true {
|
||||||
t.Fatalf("red packet payload must carry room lock snapshot: %+v", data)
|
t.Fatalf("red packet payload must carry room lock snapshot: %+v", data)
|
||||||
}
|
}
|
||||||
|
if data["owner_user_id"] != "323411852102995968" {
|
||||||
|
t.Fatalf("red packet payload must carry owner snapshot without JSON precision loss: %+v", data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDelayedRedPacketCreatedSendsRegionAndSchedulesOpenDueIM(t *testing.T) {
|
func TestDelayedRedPacketCreatedSendsRegionAndSchedulesOpenDueIM(t *testing.T) {
|
||||||
@ -568,6 +583,7 @@ func TestDelayedRedPacketCreatedSendsRegionAndSchedulesOpenDueIM(t *testing.T) {
|
|||||||
RoomID: "9001",
|
RoomID: "9001",
|
||||||
RegionID: 310,
|
RegionID: 310,
|
||||||
RoomLocked: true,
|
RoomLocked: true,
|
||||||
|
OwnerUserID: 323411852102995968,
|
||||||
SenderUserID: 10001,
|
SenderUserID: 10001,
|
||||||
TotalAmount: 15000,
|
TotalAmount: 15000,
|
||||||
TotalCount: 5,
|
TotalCount: 5,
|
||||||
@ -596,11 +612,11 @@ func TestDelayedRedPacketCreatedSendsRegionAndSchedulesOpenDueIM(t *testing.T) {
|
|||||||
t.Fatalf("open due outbox mismatch: %+v", openDue)
|
t.Fatalf("open due outbox mismatch: %+v", openDue)
|
||||||
}
|
}
|
||||||
regionData := redPacketPayloadData(t, regionCreated.PayloadJSON)
|
regionData := redPacketPayloadData(t, regionCreated.PayloadJSON)
|
||||||
if regionData["event"] != "CREATED" || regionData["scope"] != "REGION" || regionData["packetMode"] != "DELAYED" || regionData["room_locked"] != true {
|
if regionData["event"] != "CREATED" || regionData["scope"] != "REGION" || regionData["packetMode"] != "DELAYED" || regionData["room_locked"] != true || regionData["owner_user_id"] != "323411852102995968" {
|
||||||
t.Fatalf("region created payload mismatch: %+v", regionData)
|
t.Fatalf("region created payload mismatch: %+v", regionData)
|
||||||
}
|
}
|
||||||
openDueData := redPacketPayloadData(t, openDue.PayloadJSON)
|
openDueData := redPacketPayloadData(t, openDue.PayloadJSON)
|
||||||
if openDueData["event"] != "OPEN_DUE" || openDueData["scope"] != "ROOM" || openDueData["claimStartTime"] != float64(openAtMS) || openDueData["room_locked"] != true {
|
if openDueData["event"] != "OPEN_DUE" || openDueData["scope"] != "ROOM" || openDueData["claimStartTime"] != float64(openAtMS) || openDueData["room_locked"] != true || openDueData["owner_user_id"] != "323411852102995968" {
|
||||||
t.Fatalf("open due payload mismatch: %+v", openDueData)
|
t.Fatalf("open due payload mismatch: %+v", openDueData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -71,6 +71,7 @@ type fakeRoomClient struct {
|
|||||||
updateProfileErr error
|
updateProfileErr error
|
||||||
joinErr error
|
joinErr error
|
||||||
joinResp *roomv1.JoinRoomResponse
|
joinResp *roomv1.JoinRoomResponse
|
||||||
|
micUpErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
type fakeRoomVIPClient struct {
|
type fakeRoomVIPClient struct {
|
||||||
@ -219,6 +220,9 @@ func (f *fakeRoomClient) LeaveRoom(_ context.Context, req *roomv1.LeaveRoomReque
|
|||||||
|
|
||||||
func (f *fakeRoomClient) MicUp(_ context.Context, req *roomv1.MicUpRequest) (*roomv1.MicUpResponse, error) {
|
func (f *fakeRoomClient) MicUp(_ context.Context, req *roomv1.MicUpRequest) (*roomv1.MicUpResponse, error) {
|
||||||
f.lastMicUp = req
|
f.lastMicUp = req
|
||||||
|
if f.micUpErr != nil {
|
||||||
|
return nil, f.micUpErr
|
||||||
|
}
|
||||||
return &roomv1.MicUpResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 4}}, nil
|
return &roomv1.MicUpResponse{Result: &roomv1.CommandResult{Applied: true, RoomVersion: 4}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -11744,6 +11748,32 @@ func TestMapReasonToHTTPMapsGenericConflict(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMicUpReturnsExplicitRoomMutedMessage(t *testing.T) {
|
||||||
|
// Flutter 只消费 gateway envelope;这里锁定跨 gRPC 边界后的公开 code/message,避免再次退化成通用 permission denied。
|
||||||
|
client := &fakeRoomClient{
|
||||||
|
micUpErr: xerr.ToGRPCError(xerr.New(xerr.RoomUserMuted, "user is muted")),
|
||||||
|
}
|
||||||
|
router := NewHandler(client).Routes(auth.NewVerifier("secret"))
|
||||||
|
request := httptest.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
"/api/v1/rooms/mic/up",
|
||||||
|
bytes.NewReader([]byte(`{"room_id":"lalu_room-muted","command_id":"mic-up-muted","seat_no":2}`)),
|
||||||
|
)
|
||||||
|
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(recorder, request)
|
||||||
|
|
||||||
|
assertEnvelopeMessage(
|
||||||
|
t,
|
||||||
|
recorder,
|
||||||
|
http.StatusForbidden,
|
||||||
|
string(xerr.RoomUserMuted),
|
||||||
|
"You have been muted and cannot join the mic.",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRoutesWriteUnauthorizedEnvelope(t *testing.T) {
|
func TestRoutesWriteUnauthorizedEnvelope(t *testing.T) {
|
||||||
router := NewHandler(&fakeRoomClient{}).Routes(auth.NewVerifier("secret"))
|
router := NewHandler(&fakeRoomClient{}).Routes(auth.NewVerifier("secret"))
|
||||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/create", bytes.NewReader([]byte(`{}`)))
|
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/create", bytes.NewReader([]byte(`{}`)))
|
||||||
|
|||||||
@ -261,6 +261,7 @@ func (h *Handler) sendVoiceRoomRedPacket(writer http.ResponseWriter, request *ht
|
|||||||
TotalAmount: body.amount(),
|
TotalAmount: body.amount(),
|
||||||
PacketCount: body.count(),
|
PacketCount: body.count(),
|
||||||
RoomLocked: snapshotResp.GetRoom().GetLocked(),
|
RoomLocked: snapshotResp.GetRoom().GetLocked(),
|
||||||
|
OwnerUserId: snapshotResp.GetRoom().GetOwnerUserId(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeRedPacketRPCError(writer, request, err)
|
writeRedPacketRPCError(writer, request, err)
|
||||||
@ -706,6 +707,7 @@ func (h *Handler) createRoomRedPacket(writer http.ResponseWriter, request *http.
|
|||||||
TotalAmount: totalAmount,
|
TotalAmount: totalAmount,
|
||||||
PacketCount: packetCount,
|
PacketCount: packetCount,
|
||||||
RoomLocked: snapshotResp.GetRoom().GetLocked(),
|
RoomLocked: snapshotResp.GetRoom().GetLocked(),
|
||||||
|
OwnerUserId: snapshotResp.GetRoom().GetOwnerUserId(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
httpkit.WriteRPCError(writer, request, err)
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
|||||||
@ -214,7 +214,9 @@ type DrawCommand struct {
|
|||||||
UserRegisteredAtMS int64
|
UserRegisteredAtMS int64
|
||||||
VisibleRegionID int64
|
VisibleRegionID int64
|
||||||
// RoomLocked 是原始送礼时的房间展示快照,只能随 owner fact 透传,不能参与中奖概率和账务语义。
|
// RoomLocked 是原始送礼时的房间展示快照,只能随 owner fact 透传,不能参与中奖概率和账务语义。
|
||||||
RoomLocked bool
|
RoomLocked bool
|
||||||
|
// OwnerUserID 与 RoomLocked 来自同一次 Room Cell 送礼快照;它只服务跨房密码框交互,不参与抽奖资格或返奖。
|
||||||
|
OwnerUserID int64
|
||||||
CountryID int64
|
CountryID int64
|
||||||
Recharge7DCoins int64
|
Recharge7DCoins int64
|
||||||
Recharge30DCoins int64
|
Recharge30DCoins int64
|
||||||
|
|||||||
@ -73,7 +73,7 @@ func TestPublishLuckyGiftDrawnFactCarriesGrantedSnapshot(t *testing.T) {
|
|||||||
if err := proto.Unmarshal(publisher.envelope.GetBody(), &fact); err != nil {
|
if err := proto.Unmarshal(publisher.envelope.GetBody(), &fact); err != nil {
|
||||||
t.Fatalf("decode owner fact: %v", err)
|
t.Fatalf("decode owner fact: %v", err)
|
||||||
}
|
}
|
||||||
if fact.GetRewardStatus() != domain.StatusGranted || fact.GetWalletTransactionId() != "wallet-tx-1" || fact.GetMultiplierPpm() != luckygiftmq.LuckyGiftDisplayMinMultiplierPPM || fact.GetSenderName() != "Lucky Sender" || !fact.GetRoomLocked() || fact.GetRewardGrantedAtMs() != 1783940398320 {
|
if fact.GetRewardStatus() != domain.StatusGranted || fact.GetWalletTransactionId() != "wallet-tx-1" || fact.GetMultiplierPpm() != luckygiftmq.LuckyGiftDisplayMinMultiplierPPM || fact.GetSenderName() != "Lucky Sender" || !fact.GetRoomLocked() || fact.GetOwnerUserId() != 323411852102995968 || fact.GetRewardGrantedAtMs() != 1783940398320 {
|
||||||
t.Fatalf("unexpected owner fact: %+v", &fact)
|
t.Fatalf("unexpected owner fact: %+v", &fact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -165,6 +165,7 @@ func testLuckyGiftDrawnPayload() luckyGiftDrawnPayload {
|
|||||||
SenderDisplayUserID: "123456",
|
SenderDisplayUserID: "123456",
|
||||||
VisibleRegionID: 1,
|
VisibleRegionID: 1,
|
||||||
RoomLocked: true,
|
RoomLocked: true,
|
||||||
|
OwnerUserID: 323411852102995968,
|
||||||
CountryID: 86,
|
CountryID: 86,
|
||||||
CoinSpent: 100,
|
CoinSpent: 100,
|
||||||
MultiplierPPM: luckygiftmq.LuckyGiftDisplayMinMultiplierPPM,
|
MultiplierPPM: luckygiftmq.LuckyGiftDisplayMinMultiplierPPM,
|
||||||
|
|||||||
@ -526,6 +526,7 @@ func (s *Service) creditLuckyGiftRewardFastPath(ctx context.Context, cmd domain.
|
|||||||
SenderPrettyDisplayUserID: cmd.SenderPrettyDisplayUserID,
|
SenderPrettyDisplayUserID: cmd.SenderPrettyDisplayUserID,
|
||||||
VisibleRegionID: cmd.VisibleRegionID,
|
VisibleRegionID: cmd.VisibleRegionID,
|
||||||
RoomLocked: cmd.RoomLocked,
|
RoomLocked: cmd.RoomLocked,
|
||||||
|
OwnerUserID: cmd.OwnerUserID,
|
||||||
CountryID: cmd.CountryID,
|
CountryID: cmd.CountryID,
|
||||||
CoinSpent: cmd.CoinSpent,
|
CoinSpent: cmd.CoinSpent,
|
||||||
RuleVersion: result.RuleVersion,
|
RuleVersion: result.RuleVersion,
|
||||||
@ -600,6 +601,7 @@ func (s *Service) publishLuckyGiftDrawnFact(ctx context.Context, payload luckyGi
|
|||||||
SenderPrettyDisplayUserId: payload.SenderPrettyDisplayUserID,
|
SenderPrettyDisplayUserId: payload.SenderPrettyDisplayUserID,
|
||||||
VisibleRegionId: payload.VisibleRegionID,
|
VisibleRegionId: payload.VisibleRegionID,
|
||||||
RoomLocked: payload.RoomLocked,
|
RoomLocked: payload.RoomLocked,
|
||||||
|
OwnerUserId: payload.OwnerUserID,
|
||||||
CountryId: payload.CountryID,
|
CountryId: payload.CountryID,
|
||||||
CoinSpent: payload.CoinSpent,
|
CoinSpent: payload.CoinSpent,
|
||||||
RuleVersion: payload.RuleVersion,
|
RuleVersion: payload.RuleVersion,
|
||||||
@ -1086,6 +1088,7 @@ type luckyGiftDrawnPayload struct {
|
|||||||
SenderPrettyDisplayUserID string `json:"sender_pretty_display_user_id,omitempty"`
|
SenderPrettyDisplayUserID string `json:"sender_pretty_display_user_id,omitempty"`
|
||||||
VisibleRegionID int64 `json:"visible_region_id"`
|
VisibleRegionID int64 `json:"visible_region_id"`
|
||||||
RoomLocked bool `json:"room_locked"`
|
RoomLocked bool `json:"room_locked"`
|
||||||
|
OwnerUserID int64 `json:"owner_user_id,omitempty"`
|
||||||
CountryID int64 `json:"country_id"`
|
CountryID int64 `json:"country_id"`
|
||||||
CoinSpent int64 `json:"coin_spent"`
|
CoinSpent int64 `json:"coin_spent"`
|
||||||
RuleVersion int64 `json:"rule_version"`
|
RuleVersion int64 `json:"rule_version"`
|
||||||
|
|||||||
@ -1232,6 +1232,7 @@ func (r *Repository) insertLuckyAggregateRewardSettlementOutbox(ctx context.Cont
|
|||||||
"sender_pretty_display_user_id": cmd.SenderPrettyDisplayUserID,
|
"sender_pretty_display_user_id": cmd.SenderPrettyDisplayUserID,
|
||||||
"visible_region_id": cmd.VisibleRegionID,
|
"visible_region_id": cmd.VisibleRegionID,
|
||||||
"room_locked": cmd.RoomLocked,
|
"room_locked": cmd.RoomLocked,
|
||||||
|
"owner_user_id": cmd.OwnerUserID,
|
||||||
"country_id": cmd.CountryID,
|
"country_id": cmd.CountryID,
|
||||||
"room_id": cmd.RoomID,
|
"room_id": cmd.RoomID,
|
||||||
"gift_id": cmd.GiftID,
|
"gift_id": cmd.GiftID,
|
||||||
@ -1611,6 +1612,7 @@ func (r *Repository) applyLuckyDraw(ctx context.Context, tx *sql.Tx, input lucky
|
|||||||
"sender_pretty_display_user_id": input.Command.SenderPrettyDisplayUserID,
|
"sender_pretty_display_user_id": input.Command.SenderPrettyDisplayUserID,
|
||||||
"visible_region_id": input.Command.VisibleRegionID,
|
"visible_region_id": input.Command.VisibleRegionID,
|
||||||
"room_locked": input.Command.RoomLocked,
|
"room_locked": input.Command.RoomLocked,
|
||||||
|
"owner_user_id": input.Command.OwnerUserID,
|
||||||
"country_id": input.Command.CountryID,
|
"country_id": input.Command.CountryID,
|
||||||
"room_id": input.Command.RoomID,
|
"room_id": input.Command.RoomID,
|
||||||
"gift_id": input.Command.GiftID,
|
"gift_id": input.Command.GiftID,
|
||||||
|
|||||||
@ -123,6 +123,7 @@ func luckyDrawCommandFromProto(meta *luckygiftv1.LuckyGiftMeta) domain.DrawComma
|
|||||||
UserRegisteredAtMS: meta.GetUserRegisteredAtMs(),
|
UserRegisteredAtMS: meta.GetUserRegisteredAtMs(),
|
||||||
VisibleRegionID: meta.GetVisibleRegionId(),
|
VisibleRegionID: meta.GetVisibleRegionId(),
|
||||||
RoomLocked: meta.GetRoomLocked(),
|
RoomLocked: meta.GetRoomLocked(),
|
||||||
|
OwnerUserID: meta.GetOwnerUserId(),
|
||||||
CountryID: meta.GetCountryId(),
|
CountryID: meta.GetCountryId(),
|
||||||
SenderName: meta.GetSenderName(),
|
SenderName: meta.GetSenderName(),
|
||||||
SenderAvatar: meta.GetSenderAvatar(),
|
SenderAvatar: meta.GetSenderAvatar(),
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import (
|
|||||||
"hyapp/services/room-service/internal/room/state"
|
"hyapp/services/room-service/internal/room/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
func buildRoomGiftSentRecords(roomID string, roomVersion int64, now time.Time, cmd command.SendGift, roomMeta RoomMeta, roomLocked bool, targetBillings []giftTargetBilling, targetCurrentGiftValues map[int64]int64) ([]outbox.Record, error) {
|
func buildRoomGiftSentRecords(roomID string, roomVersion int64, now time.Time, cmd command.SendGift, roomMeta RoomMeta, roomLocked bool, ownerUserID int64, targetBillings []giftTargetBilling, targetCurrentGiftValues map[int64]int64) ([]outbox.Record, error) {
|
||||||
// RoomGiftSent 是统计、活动、成长任务和房间展示共同消费的送礼事实;构造时必须按目标拆分,不能用 batch 展示消息替代逐目标账务事实。
|
// RoomGiftSent 是统计、活动、成长任务和房间展示共同消费的送礼事实;构造时必须按目标拆分,不能用 batch 展示消息替代逐目标账务事实。
|
||||||
records := make([]outbox.Record, 0, len(targetBillings))
|
records := make([]outbox.Record, 0, len(targetBillings))
|
||||||
for _, targetBilling := range targetBillings {
|
for _, targetBilling := range targetBillings {
|
||||||
@ -49,6 +49,7 @@ func buildRoomGiftSentRecords(roomID string, roomVersion int64, now time.Time, c
|
|||||||
// effect_types 来自 wallet-service 已锁定的礼物价格配置;room-service 只透传,避免统计和全服广播各自重新判定礼物效果。
|
// effect_types 来自 wallet-service 已锁定的礼物价格配置;room-service 只透传,避免统计和全服广播各自重新判定礼物效果。
|
||||||
GiftEffectTypes: targetBilling.Billing.GetGiftEffectTypes(),
|
GiftEffectTypes: targetBilling.Billing.GetGiftEffectTypes(),
|
||||||
RoomLocked: roomLocked,
|
RoomLocked: roomLocked,
|
||||||
|
OwnerUserId: ownerUserID,
|
||||||
IsRobotGift: cmd.RobotWalletGift,
|
IsRobotGift: cmd.RobotWalletGift,
|
||||||
SyntheticLuckyGift: cmd.SyntheticLuckyGift,
|
SyntheticLuckyGift: cmd.SyntheticLuckyGift,
|
||||||
RealRoomRobotGift: cmd.RobotWalletGift && !cmd.RobotGift,
|
RealRoomRobotGift: cmd.RobotWalletGift && !cmd.RobotGift,
|
||||||
|
|||||||
@ -60,7 +60,7 @@ func TestBuildRoomGiftSentRecordsPreservesDurableFactFields(t *testing.T) {
|
|||||||
GiftEffectTypes: []string{"room_banner", "global_broadcast"},
|
GiftEffectTypes: []string{"room_banner", "global_broadcast"},
|
||||||
}
|
}
|
||||||
|
|
||||||
records, err := buildRoomGiftSentRecords("room-gift-fact", 7, now, cmd, RoomMeta{VisibleRegionID: 8008}, true, []giftTargetBilling{{
|
records, err := buildRoomGiftSentRecords("room-gift-fact", 7, now, cmd, RoomMeta{VisibleRegionID: 8008}, true, 303, []giftTargetBilling{{
|
||||||
TargetUserID: 202,
|
TargetUserID: 202,
|
||||||
CommandID: "cmd-gift-fact:202",
|
CommandID: "cmd-gift-fact:202",
|
||||||
Billing: billing,
|
Billing: billing,
|
||||||
@ -86,7 +86,7 @@ func TestBuildRoomGiftSentRecordsPreservesDurableFactFields(t *testing.T) {
|
|||||||
if event.GetBillingReceiptId() != "receipt-target-202" || event.GetCoinSpent() != 300 || event.GetGiftValue() != 450 || event.GetTargetGiftValue() != 999 {
|
if event.GetBillingReceiptId() != "receipt-target-202" || event.GetCoinSpent() != 300 || event.GetGiftValue() != 450 || event.GetTargetGiftValue() != 999 {
|
||||||
t.Fatalf("billing fields mismatch: %+v", &event)
|
t.Fatalf("billing fields mismatch: %+v", &event)
|
||||||
}
|
}
|
||||||
if event.GetVisibleRegionId() != 8008 || event.GetCountryId() != 86 || event.GetRegionId() != 9001 || event.GetCommandId() != "cmd-gift-fact:202" || !event.GetRoomLocked() {
|
if event.GetVisibleRegionId() != 8008 || event.GetCountryId() != 86 || event.GetRegionId() != 9001 || event.GetCommandId() != "cmd-gift-fact:202" || !event.GetRoomLocked() || event.GetOwnerUserId() != 303 {
|
||||||
t.Fatalf("routing/stat fields mismatch: %+v", &event)
|
t.Fatalf("routing/stat fields mismatch: %+v", &event)
|
||||||
}
|
}
|
||||||
if event.GetGiftTypeCode() != "lucky" || event.GetCpRelationType() != "couple" || event.GetGiftName() != "Rose" {
|
if event.GetGiftTypeCode() != "lucky" || event.GetCpRelationType() != "couple" || event.GetGiftName() != "Rose" {
|
||||||
@ -127,7 +127,7 @@ func TestBuildRoomGiftSentRecordsKeepsBatchDisplayFactsSplitPerTarget(t *testing
|
|||||||
HeatValue: 100,
|
HeatValue: 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
records, err := buildRoomGiftSentRecords("room-gift-batch-facts", 9, now, cmd, RoomMeta{}, false, []giftTargetBilling{
|
records, err := buildRoomGiftSentRecords("room-gift-batch-facts", 9, now, cmd, RoomMeta{}, false, 303, []giftTargetBilling{
|
||||||
{TargetUserID: 202, CommandID: "cmd-gift-batch-facts:202", Billing: billing},
|
{TargetUserID: 202, CommandID: "cmd-gift-batch-facts:202", Billing: billing},
|
||||||
{TargetUserID: 203, CommandID: "cmd-gift-batch-facts:203", Billing: billing},
|
{TargetUserID: 203, CommandID: "cmd-gift-batch-facts:203", Billing: billing},
|
||||||
}, nil)
|
}, nil)
|
||||||
@ -142,7 +142,7 @@ func TestBuildRoomGiftSentRecordsKeepsBatchDisplayFactsSplitPerTarget(t *testing
|
|||||||
if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
|
if err := proto.Unmarshal(record.Envelope.GetBody(), &event); err != nil {
|
||||||
t.Fatalf("unmarshal batch RoomGiftSent failed: %v", err)
|
t.Fatalf("unmarshal batch RoomGiftSent failed: %v", err)
|
||||||
}
|
}
|
||||||
if event.GetTargetUserId() <= 0 || event.GetCommandId() == "" {
|
if event.GetTargetUserId() <= 0 || event.GetCommandId() == "" || event.GetOwnerUserId() != 303 {
|
||||||
t.Fatalf("batch RoomGiftSent fact must keep per-target fields: %+v", &event)
|
t.Fatalf("batch RoomGiftSent fact must keep per-target fields: %+v", &event)
|
||||||
}
|
}
|
||||||
if event.GetDisplayMode() != "batch" {
|
if event.GetDisplayMode() != "batch" {
|
||||||
|
|||||||
@ -29,6 +29,8 @@ type giftFlow struct {
|
|||||||
cmd command.SendGift
|
cmd command.SendGift
|
||||||
|
|
||||||
roomMeta RoomMeta
|
roomMeta RoomMeta
|
||||||
|
// ownerUserID 与 roomLocked 在同一 Room Cell 串行区间读取,RoomGiftSent 必须复用该快照。
|
||||||
|
ownerUserID int64
|
||||||
// roomLocked 在进入钱包/幸运礼物副作用前从同一个 Room Cell 快照固化,保证后续所有飘屏描述同一时刻的锁房状态。
|
// roomLocked 在进入钱包/幸运礼物副作用前从同一个 Room Cell 快照固化,保证后续所有飘屏描述同一时刻的锁房状态。
|
||||||
roomLocked bool
|
roomLocked bool
|
||||||
rocketConfig RoomRocketConfig
|
rocketConfig RoomRocketConfig
|
||||||
@ -112,6 +114,8 @@ func newGiftFlow(s *Service, ctx context.Context, req *roomv1.SendGiftRequest, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *giftFlow) validateBeforeDebit(current *state.RoomState) error {
|
func (f *giftFlow) validateBeforeDebit(current *state.RoomState) error {
|
||||||
|
// 普通礼物事实的房主身份属于 Room Cell 核心状态;异步 activity 不能用发送者或收礼人替代。
|
||||||
|
f.ownerUserID = current.OwnerUserID
|
||||||
// 新命令在 Room Cell 串行区间固化锁状态;恢复优先复用首次快照。历史 saga 没有该字段时才用当前 Cell 状态兼容补齐。
|
// 新命令在 Room Cell 串行区间固化锁状态;恢复优先复用首次快照。历史 saga 没有该字段时才用当前 Cell 状态兼容补齐。
|
||||||
if f.recovering && f.cmd.RoomLocked != nil {
|
if f.recovering && f.cmd.RoomLocked != nil {
|
||||||
f.roomLocked = *f.cmd.RoomLocked
|
f.roomLocked = *f.cmd.RoomLocked
|
||||||
@ -367,7 +371,7 @@ func (f *giftFlow) applyRoomState(now time.Time, current *state.RoomState, local
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *giftFlow) buildMutationResult(now time.Time, current *state.RoomState) (mutationResult, []outbox.Record, error) {
|
func (f *giftFlow) buildMutationResult(now time.Time, current *state.RoomState) (mutationResult, []outbox.Record, error) {
|
||||||
giftEvents, err := buildRoomGiftSentRecords(current.RoomID, current.Version, now, f.cmd, f.roomMeta, f.roomLocked, f.targetBillings, f.targetCurrentGiftValues)
|
giftEvents, err := buildRoomGiftSentRecords(current.RoomID, current.Version, now, f.cmd, f.roomMeta, f.roomLocked, f.ownerUserID, f.targetBillings, f.targetCurrentGiftValues)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mutationResult{}, nil, err
|
return mutationResult{}, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,7 +116,9 @@ func luckyGiftMetaForTarget(ctx context.Context, meta *roomv1.RequestMeta, cmd c
|
|||||||
VisibleRegionId: roomMeta.VisibleRegionID,
|
VisibleRegionId: roomMeta.VisibleRegionID,
|
||||||
// 锁房状态与 presence 校验取自同一个串行 Room Cell 快照;lucky owner 只持久化展示事实,不能跨库回查房间。
|
// 锁房状态与 presence 校验取自同一个串行 Room Cell 快照;lucky owner 只持久化展示事实,不能跨库回查房间。
|
||||||
RoomLocked: roomLocked,
|
RoomLocked: roomLocked,
|
||||||
CountryId: cmd.SenderCountryID,
|
// 幸运礼物使用原始送礼持久 RoomMeta 的房主身份;发送者、中奖人和异步回查结果都不能替代该快照。
|
||||||
|
OwnerUserId: roomMeta.OwnerUserID,
|
||||||
|
CountryId: cmd.SenderCountryID,
|
||||||
// 账号创建时间来自 gateway 的 user-service 快照并随首次命令持久化;owner 只用它判断
|
// 账号创建时间来自 gateway 的 user-service 快照并随首次命令持久化;owner 只用它判断
|
||||||
// 支付时账号是否已满完整历史窗口。0 表示旧命令/缺失事实,补偿大奖资格必须 fail-close。
|
// 支付时账号是否已满完整历史窗口。0 表示旧命令/缺失事实,补偿大奖资格必须 fail-close。
|
||||||
UserRegisteredAtMs: cmd.SenderRegisteredAtMS,
|
UserRegisteredAtMs: cmd.SenderRegisteredAtMS,
|
||||||
|
|||||||
@ -130,7 +130,7 @@ func (s *Service) RobotSendGift(ctx context.Context, input RobotSendGiftInput) (
|
|||||||
records, err := buildRoomGiftSentRecords(flow.cmd.RoomID(), snapshot.GetVersion(), now, flow.cmd, RoomMeta{
|
records, err := buildRoomGiftSentRecords(flow.cmd.RoomID(), snapshot.GetVersion(), now, flow.cmd, RoomMeta{
|
||||||
RoomID: flow.cmd.RoomID(),
|
RoomID: flow.cmd.RoomID(),
|
||||||
VisibleRegionID: snapshot.GetVisibleRegionId(),
|
VisibleRegionID: snapshot.GetVisibleRegionId(),
|
||||||
}, snapshot.GetLocked(), targetBillings, targetGiftValues)
|
}, snapshot.GetLocked(), snapshot.GetOwnerUserId(), targetBillings, targetGiftValues)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,7 @@ func (s *Service) HandleLuckyGiftOutboxEvent(ctx context.Context, envelope *luck
|
|||||||
SenderPrettyDisplayUserId: drawn.GetSenderPrettyDisplayUserId(),
|
SenderPrettyDisplayUserId: drawn.GetSenderPrettyDisplayUserId(),
|
||||||
VisibleRegionId: drawn.GetVisibleRegionId(),
|
VisibleRegionId: drawn.GetVisibleRegionId(),
|
||||||
RoomLocked: drawn.GetRoomLocked(),
|
RoomLocked: drawn.GetRoomLocked(),
|
||||||
|
OwnerUserId: drawn.GetOwnerUserId(),
|
||||||
CountryId: drawn.GetCountryId(),
|
CountryId: drawn.GetCountryId(),
|
||||||
CoinSpent: drawn.GetCoinSpent(),
|
CoinSpent: drawn.GetCoinSpent(),
|
||||||
RuleVersion: drawn.GetRuleVersion(),
|
RuleVersion: drawn.GetRuleVersion(),
|
||||||
|
|||||||
@ -83,7 +83,7 @@ func TestHandleLuckyGiftOutboxEventPersistsOneDerivedRoomDisplayOutbox(t *testin
|
|||||||
if err := proto.Unmarshal(record.Envelope.GetBody(), &body); err != nil {
|
if err := proto.Unmarshal(record.Envelope.GetBody(), &body); err != nil {
|
||||||
t.Fatalf("decode derived RoomLuckyGiftDrawn failed: %v", err)
|
t.Fatalf("decode derived RoomLuckyGiftDrawn failed: %v", err)
|
||||||
}
|
}
|
||||||
if body.GetDrawId() != "lucky_draw_room_1" || body.GetSenderName() != "Sender" || body.GetMultiplierPpm() != luckygiftmq.LuckyGiftDisplayMinMultiplierPPM || body.GetRewardStatus() != "granted" || !body.GetRoomLocked() {
|
if body.GetDrawId() != "lucky_draw_room_1" || body.GetSenderName() != "Sender" || body.GetMultiplierPpm() != luckygiftmq.LuckyGiftDisplayMinMultiplierPPM || body.GetRewardStatus() != "granted" || !body.GetRoomLocked() || body.GetOwnerUserId() != 323411852102995968 {
|
||||||
t.Fatalf("derived RoomLuckyGiftDrawn mismatch: %+v", &body)
|
t.Fatalf("derived RoomLuckyGiftDrawn mismatch: %+v", &body)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,6 +132,7 @@ func luckyGiftDisplayEnvelope(t *testing.T, multiplierPPM int64) *luckygiftevent
|
|||||||
SenderPrettyDisplayUserId: "VIP123456",
|
SenderPrettyDisplayUserId: "VIP123456",
|
||||||
VisibleRegionId: 1,
|
VisibleRegionId: 1,
|
||||||
RoomLocked: true,
|
RoomLocked: true,
|
||||||
|
OwnerUserId: 323411852102995968,
|
||||||
CountryId: 86,
|
CountryId: 86,
|
||||||
CoinSpent: 100,
|
CoinSpent: 100,
|
||||||
RuleVersion: 3,
|
RuleVersion: 3,
|
||||||
|
|||||||
@ -39,7 +39,7 @@ func (s *Service) MicUp(ctx context.Context, req *roomv1.MicUpRequest) (*roomv1.
|
|||||||
}
|
}
|
||||||
if current.MuteUsers[cmd.ActorUserID()] {
|
if current.MuteUsers[cmd.ActorUserID()] {
|
||||||
// 禁言同时约束公屏和新上麦;校验必须落在 Room Cell 命令内,不能依赖客户端收到事件后自行拦截。
|
// 禁言同时约束公屏和新上麦;校验必须落在 Room Cell 命令内,不能依赖客户端收到事件后自行拦截。
|
||||||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is muted")
|
return mutationResult{}, nil, xerr.New(xerr.RoomUserMuted, "user is muted")
|
||||||
}
|
}
|
||||||
|
|
||||||
if seat, exists := current.SeatByUser(cmd.ActorUserID()); exists {
|
if seat, exists := current.SeatByUser(cmd.ActorUserID()); exists {
|
||||||
@ -379,7 +379,7 @@ func (s *Service) ConfirmMicPublishing(ctx context.Context, req *roomv1.ConfirmM
|
|||||||
}
|
}
|
||||||
if current.MuteUsers[cmd.TargetUserID] {
|
if current.MuteUsers[cmd.TargetUserID] {
|
||||||
// MicUp 后到 RTC 发流确认之间仍可能发生房间禁言;确认入口必须再次校验,不能复活已撤销的发言权限。
|
// MicUp 后到 RTC 发流确认之间仍可能发生房间禁言;确认入口必须再次校验,不能复活已撤销的发言权限。
|
||||||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is muted")
|
return mutationResult{}, nil, xerr.New(xerr.RoomUserMuted, "user is muted")
|
||||||
}
|
}
|
||||||
|
|
||||||
seat, exists := current.SeatByUser(cmd.TargetUserID)
|
seat, exists := current.SeatByUser(cmd.TargetUserID)
|
||||||
@ -614,7 +614,7 @@ func (s *Service) SetMicMute(ctx context.Context, req *roomv1.SetMicMuteRequest)
|
|||||||
}
|
}
|
||||||
if !cmd.Muted && current.MuteUsers[cmd.TargetUserID] {
|
if !cmd.Muted && current.MuteUsers[cmd.TargetUserID] {
|
||||||
// 房间禁言优先级高于用户自己的麦克风开关;解除房间禁言前禁止目标或管理员把 RTC 麦位重新打开。
|
// 房间禁言优先级高于用户自己的麦克风开关;解除房间禁言前禁止目标或管理员把 RTC 麦位重新打开。
|
||||||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "user is muted")
|
return mutationResult{}, nil, xerr.New(xerr.RoomUserMuted, "user is muted")
|
||||||
}
|
}
|
||||||
if seat.MicMuted == cmd.Muted {
|
if seat.MicMuted == cmd.Muted {
|
||||||
return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
|
return mutationResult{snapshot: current.ToProto(), seatNo: seat.SeatNo}, nil, nil
|
||||||
|
|||||||
@ -101,8 +101,8 @@ func TestMutedUserCannotSpeakOrMicUpUntilUnmuted(t *testing.T) {
|
|||||||
if _, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
|
if _, err := svc.MicUp(ctx, &roomv1.MicUpRequest{
|
||||||
Meta: rocketMeta(roomID, mutedUserID, "muted-mic-up"),
|
Meta: rocketMeta(roomID, mutedUserID, "muted-mic-up"),
|
||||||
SeatNo: 2,
|
SeatNo: 2,
|
||||||
}); !xerr.IsCode(err, xerr.PermissionDenied) {
|
}); !xerr.IsCode(err, xerr.RoomUserMuted) {
|
||||||
t.Fatalf("muted user mic up must return permission denied: %v", err)
|
t.Fatalf("muted user mic up must return room user muted: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := svc.MuteUser(ctx, &roomv1.MuteUserRequest{
|
if _, err := svc.MuteUser(ctx, &roomv1.MuteUserRequest{
|
||||||
@ -175,7 +175,7 @@ func TestMutedUserCannotSpeakOrMicUpUntilUnmuted(t *testing.T) {
|
|||||||
RoomVersion: upResp.GetResult().GetRoomVersion(),
|
RoomVersion: upResp.GetResult().GetRoomVersion(),
|
||||||
EventTimeMs: now.Now().UnixMilli(),
|
EventTimeMs: now.Now().UnixMilli(),
|
||||||
Source: "client",
|
Source: "client",
|
||||||
}); !xerr.IsCode(err, xerr.PermissionDenied) {
|
}); !xerr.IsCode(err, xerr.RoomUserMuted) {
|
||||||
t.Fatalf("room mute between MicUp and RTC confirmation must reject publishing: %v", err)
|
t.Fatalf("room mute between MicUp and RTC confirmation must reject publishing: %v", err)
|
||||||
}
|
}
|
||||||
rtcEventResp, err := svc.ApplyRTCEvent(ctx, &roomv1.ApplyRTCEventRequest{
|
rtcEventResp, err := svc.ApplyRTCEvent(ctx, &roomv1.ApplyRTCEventRequest{
|
||||||
|
|||||||
@ -438,6 +438,8 @@ func (s *Service) applyRoomRocketGift(now time.Time, current *state.RoomState, c
|
|||||||
RoomShortId: roomMeta.RoomShortID,
|
RoomShortId: roomMeta.RoomShortID,
|
||||||
RocketCoverUrl: pending.CoverURL,
|
RocketCoverUrl: pending.CoverURL,
|
||||||
ResetAtMs: pending.ResetAtMS,
|
ResetAtMs: pending.ResetAtMS,
|
||||||
|
// 构造函数同时服务普通送礼和 RPS 礼物;在这里读取 Cell 房主才能保证两条点火链路不漏字段。
|
||||||
|
OwnerUserId: current.OwnerUserID,
|
||||||
}
|
}
|
||||||
if rocket.CurrentLevel < roomRocketLevelCount {
|
if rocket.CurrentLevel < roomRocketLevelCount {
|
||||||
nextLevel := rocket.CurrentLevel + 1
|
nextLevel := rocket.CurrentLevel + 1
|
||||||
|
|||||||
@ -1340,7 +1340,7 @@ func TestRoomRocketGiftOverflowAndImmediateNextLevel(t *testing.T) {
|
|||||||
waitForRoomDirectIMCounts(t, directIM, map[string]int{"RoomRocketIgnited": 1})
|
waitForRoomDirectIMCounts(t, directIM, map[string]int{"RoomRocketIgnited": 1})
|
||||||
ignitedRecords := directIM.recordsByType("RoomRocketIgnited")
|
ignitedRecords := directIM.recordsByType("RoomRocketIgnited")
|
||||||
var ignited roomeventsv1.RoomRocketIgnited
|
var ignited roomeventsv1.RoomRocketIgnited
|
||||||
if len(ignitedRecords) != 1 || proto.Unmarshal(ignitedRecords[0].GetBody(), &ignited) != nil || !ignited.GetRoomLocked() {
|
if len(ignitedRecords) != 1 || proto.Unmarshal(ignitedRecords[0].GetBody(), &ignited) != nil || !ignited.GetRoomLocked() || ignited.GetOwnerUserId() != ownerID {
|
||||||
t.Fatalf("rocket ignition must carry the Room Cell lock snapshot: records=%+v event=%+v", ignitedRecords, &ignited)
|
t.Fatalf("rocket ignition must carry the Room Cell lock snapshot: records=%+v event=%+v", ignitedRecords, &ignited)
|
||||||
}
|
}
|
||||||
if got := outboxEventCounts(t, ctx, repository)["RoomRocketIgnited"]; got != 1 {
|
if got := outboxEventCounts(t, ctx, repository)["RoomRocketIgnited"]; got != 1 {
|
||||||
|
|||||||
@ -156,6 +156,7 @@ CREATE TABLE IF NOT EXISTS stat_social_user_day (
|
|||||||
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '事件发生时区域维度',
|
region_id BIGINT NOT NULL DEFAULT 0 COMMENT '事件发生时区域维度',
|
||||||
user_role VARCHAR(16) NOT NULL DEFAULT 'user' COMMENT '当日用户身份快照,user 或 host',
|
user_role VARCHAR(16) NOT NULL DEFAULT 'user' COMMENT '当日用户身份快照,user 或 host',
|
||||||
app_first_open TINYINT(1) NOT NULL DEFAULT 0 COMMENT '首次打开 App 去重标记',
|
app_first_open TINYINT(1) NOT NULL DEFAULT 0 COMMENT '首次打开 App 去重标记',
|
||||||
|
profile_setup_view TINYINT(1) NOT NULL DEFAULT 0 COMMENT '填写资料页曝光去重标记',
|
||||||
registered_success TINYINT(1) NOT NULL DEFAULT 0 COMMENT '注册成功去重标记',
|
registered_success TINYINT(1) NOT NULL DEFAULT 0 COMMENT '注册成功去重标记',
|
||||||
active_user TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当日活跃去重标记',
|
active_user TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当日活跃去重标记',
|
||||||
room_join_success TINYINT(1) NOT NULL DEFAULT 0 COMMENT '进房成功去重标记',
|
room_join_success TINYINT(1) NOT NULL DEFAULT 0 COMMENT '进房成功去重标记',
|
||||||
|
|||||||
@ -0,0 +1,73 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
SET time_zone = '+00:00';
|
||||||
|
|
||||||
|
-- 单个带默认值的标记列在 MySQL 8 使用 INSTANT 元数据变更,不重写用户日宽表。
|
||||||
|
-- 生产执行前仍需确认 information_schema 与目标 MySQL 版本。
|
||||||
|
SET @add_profile_setup_view := (
|
||||||
|
SELECT IF(
|
||||||
|
COUNT(*) = 0,
|
||||||
|
'ALTER TABLE stat_social_user_day ADD COLUMN profile_setup_view TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''填写资料页曝光去重标记'' AFTER app_first_open, ALGORITHM=INSTANT',
|
||||||
|
'SELECT 1'
|
||||||
|
)
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'stat_social_user_day'
|
||||||
|
AND COLUMN_NAME = 'profile_setup_view'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @add_profile_setup_view;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- profile_start 自 2026-07-09 客户端漏斗埋点上线后已经写入 statistics-service 原始事件表,
|
||||||
|
-- 但旧消费者尚未生成 Social 日投影。回灌固定从功能上线日开始,并同时生成 UTC/中国自然日两份;
|
||||||
|
-- FORCE INDEX 的等值 App+事件名和时间下界可使用 idx_app_tracking_event_window,避免全表扫描。
|
||||||
|
-- 同一主体日的多次曝光由主键和 GREATEST 收敛,迁移重复执行不会重复计数。
|
||||||
|
SET @profile_start_from_ms = UNIX_TIMESTAMP('2026-07-09 00:00:00') * 1000;
|
||||||
|
SET @profile_start_to_ms = UNIX_TIMESTAMP(UTC_TIMESTAMP(6)) * 1000;
|
||||||
|
|
||||||
|
INSERT INTO stat_social_user_day (
|
||||||
|
app_code, stat_tz, stat_day, subject_key, user_id, device_id,
|
||||||
|
country_id, region_id, user_role, profile_setup_view, updated_at_ms
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
event.app_code,
|
||||||
|
timezone_scope.stat_tz,
|
||||||
|
DATE(FROM_UNIXTIME((event.occurred_at_ms + timezone_scope.offset_ms) / 1000)),
|
||||||
|
CASE
|
||||||
|
WHEN event.user_id > 0 THEN CONCAT('u:', event.user_id)
|
||||||
|
ELSE CONCAT('d:', event.device_id)
|
||||||
|
END,
|
||||||
|
event.user_id,
|
||||||
|
event.device_id,
|
||||||
|
event.country_id,
|
||||||
|
event.region_id,
|
||||||
|
'user',
|
||||||
|
1,
|
||||||
|
event.received_at_ms
|
||||||
|
FROM app_tracking_events event FORCE INDEX (idx_app_tracking_event_window)
|
||||||
|
CROSS JOIN (
|
||||||
|
SELECT 'UTC' AS stat_tz, 0 AS offset_ms
|
||||||
|
UNION ALL
|
||||||
|
SELECT 'Asia/Shanghai' AS stat_tz, 28800000 AS offset_ms
|
||||||
|
) timezone_scope
|
||||||
|
WHERE event.app_code IN ('lalu', 'huwaa', 'fami')
|
||||||
|
AND event.event_name = 'profile_start'
|
||||||
|
AND event.occurred_at_ms >= @profile_start_from_ms
|
||||||
|
AND event.occurred_at_ms < @profile_start_to_ms
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
user_id = CASE WHEN VALUES(user_id) > 0 THEN VALUES(user_id) ELSE user_id END,
|
||||||
|
device_id = CASE WHEN VALUES(device_id) <> '' THEN VALUES(device_id) ELSE device_id END,
|
||||||
|
country_id = CASE WHEN VALUES(country_id) > 0 THEN VALUES(country_id) ELSE country_id END,
|
||||||
|
region_id = CASE WHEN VALUES(region_id) > 0 THEN VALUES(region_id) ELSE region_id END,
|
||||||
|
profile_setup_view = GREATEST(profile_setup_view, VALUES(profile_setup_view)),
|
||||||
|
updated_at_ms = GREATEST(updated_at_ms, VALUES(updated_at_ms));
|
||||||
|
|
||||||
|
SELECT ROW_COUNT() AS social_profile_setup_view_backfill_affected_rows;
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS social_profile_setup_view_column_count
|
||||||
|
FROM information_schema.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE()
|
||||||
|
AND TABLE_NAME = 'stat_social_user_day'
|
||||||
|
AND COLUMN_NAME = 'profile_setup_view';
|
||||||
@ -531,6 +531,38 @@ func TestSocialIdentityCanonicalizesAnonymousAndLoggedSession(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProfileSetupViewMetricCanonicalizesAndDeduplicatesDailyExposure(t *testing.T) {
|
||||||
|
const day = "2026-07-23"
|
||||||
|
delta, ok := socialDeltaForAppTrackingEvent(AppTrackingEvent{
|
||||||
|
AppCode: "lalu",
|
||||||
|
EventName: "profile_start",
|
||||||
|
DeviceID: "device-profile",
|
||||||
|
OccurredAtMS: time.Date(2026, 7, 23, 0, 0, 0, 0, time.UTC).UnixMilli(),
|
||||||
|
}, statDayScope{tz: "Asia/Shanghai", day: day}, time.Now().UnixMilli())
|
||||||
|
if !ok || delta.ProfileSetupView != 1 || delta.Active != 0 {
|
||||||
|
t.Fatalf("profile_start delta mismatch: ok=%v delta=%+v", ok, delta)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := []socialRequirementUserDay{
|
||||||
|
{StatDay: day, Subject: "d:device-profile", DeviceID: "device-profile", ProfileSetupView: 1},
|
||||||
|
{StatDay: day, Subject: "u:123", UserID: 123, DirectUser: true, DeviceID: "device-profile", ProfileSetupView: 1},
|
||||||
|
}
|
||||||
|
canonical := canonicalizeSocialUserDays(rows, map[socialIdentityDeviceDayKey]int64{
|
||||||
|
{StatDay: day, DeviceID: "device-profile"}: 123,
|
||||||
|
}, 0, 0, nil)
|
||||||
|
if len(canonical) != 1 || canonical[0].ProfileSetupView != 1 {
|
||||||
|
t.Fatalf("profile setup exposure was not canonicalized once: %+v", canonical)
|
||||||
|
}
|
||||||
|
metrics := socialSectionMetrics(
|
||||||
|
SocialSectionNewUsers,
|
||||||
|
aggregateSocialRows(canonical, SocialSectionNewUsers, "all", "all", "all"),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if metrics["profile_setup_view_users"] != int64(1) {
|
||||||
|
t.Fatalf("profile setup exposure metric mismatch: %+v", metrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSocialIdentityMarksConcurrentAccountSwitchAmbiguous(t *testing.T) {
|
func TestSocialIdentityMarksConcurrentAccountSwitchAmbiguous(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
_, file, _, _ := runtime.Caller(0)
|
_, file, _, _ := runtime.Caller(0)
|
||||||
@ -740,6 +772,9 @@ func TestQuerySocialRequirementsAggregatesP0ToP5(t *testing.T) {
|
|||||||
for _, event := range []AppTrackingEvent{
|
for _, event := range []AppTrackingEvent{
|
||||||
{AppCode: "lalu", EventID: "social:first-open:1001", EventName: "app_first_open", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, Success: true, OccurredAtMS: start.UnixMilli()},
|
{AppCode: "lalu", EventID: "social:first-open:1001", EventName: "app_first_open", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, Success: true, OccurredAtMS: start.UnixMilli()},
|
||||||
{AppCode: "lalu", EventID: "social:first-open:1002", EventName: "app_first_open", UserID: user2, DeviceID: "dev-1002", CountryID: 86, RegionID: 210, Success: true, OccurredAtMS: start.UnixMilli()},
|
{AppCode: "lalu", EventID: "social:first-open:1002", EventName: "app_first_open", UserID: user2, DeviceID: "dev-1002", CountryID: 86, RegionID: 210, Success: true, OccurredAtMS: start.UnixMilli()},
|
||||||
|
{AppCode: "lalu", EventID: "social:profile-start:1001", EventName: "profile_start", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, Success: true, OccurredAtMS: start.Add(time.Minute).UnixMilli()},
|
||||||
|
{AppCode: "lalu", EventID: "social:profile-start:1001-repeat", EventName: "profile_start", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, Success: true, OccurredAtMS: start.Add(90 * time.Second).UnixMilli()},
|
||||||
|
{AppCode: "lalu", EventID: "social:profile-start:1002", EventName: "profile_start", UserID: user2, DeviceID: "dev-1002", CountryID: 86, RegionID: 210, Success: true, OccurredAtMS: start.Add(2 * time.Minute).UnixMilli()},
|
||||||
{AppCode: "lalu", EventID: "social:app-session:1001", EventName: "app_session", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, DurationMS: 600_000, Success: true, OccurredAtMS: start.Add(2 * time.Minute).UnixMilli()},
|
{AppCode: "lalu", EventID: "social:app-session:1001", EventName: "app_session", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, DurationMS: 600_000, Success: true, OccurredAtMS: start.Add(2 * time.Minute).UnixMilli()},
|
||||||
{AppCode: "lalu", EventID: "social:room-stay:1001", EventName: "room_stay", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, DurationMS: 300_000, Success: true, OccurredAtMS: start.Add(3 * time.Minute).UnixMilli()},
|
{AppCode: "lalu", EventID: "social:room-stay:1001", EventName: "room_stay", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, DurationMS: 300_000, Success: true, OccurredAtMS: start.Add(3 * time.Minute).UnixMilli()},
|
||||||
{AppCode: "lalu", EventID: "social:gift-panel:1001", EventName: "gift_panel_open", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, Success: true, OccurredAtMS: start.Add(4 * time.Minute).UnixMilli()},
|
{AppCode: "lalu", EventID: "social:gift-panel:1001", EventName: "gift_panel_open", UserID: user1, DeviceID: "dev-1001", CountryID: 86, RegionID: 210, Success: true, OccurredAtMS: start.Add(4 * time.Minute).UnixMilli()},
|
||||||
@ -827,6 +862,7 @@ func TestQuerySocialRequirementsAggregatesP0ToP5(t *testing.T) {
|
|||||||
}
|
}
|
||||||
newUsers := socialTestSection(t, result, SocialSectionNewUsers)
|
newUsers := socialTestSection(t, result, SocialSectionNewUsers)
|
||||||
if socialMetricInt(t, newUsers.Total, "app_download_users") != 2 ||
|
if socialMetricInt(t, newUsers.Total, "app_download_users") != 2 ||
|
||||||
|
socialMetricInt(t, newUsers.Total, "profile_setup_view_users") != 2 ||
|
||||||
socialMetricInt(t, newUsers.Total, "registered_users") != 2 ||
|
socialMetricInt(t, newUsers.Total, "registered_users") != 2 ||
|
||||||
socialMetricInt(t, newUsers.Total, "new_host_users") != 1 ||
|
socialMetricInt(t, newUsers.Total, "new_host_users") != 1 ||
|
||||||
socialMetricInt(t, newUsers.Total, "new_normal_users") != 1 ||
|
socialMetricInt(t, newUsers.Total, "new_normal_users") != 1 ||
|
||||||
@ -839,6 +875,15 @@ func TestQuerySocialRequirementsAggregatesP0ToP5(t *testing.T) {
|
|||||||
socialMetricInt(t, newUsers.Total, "new_user_recharge_usd_minor") != 2100 {
|
socialMetricInt(t, newUsers.Total, "new_user_recharge_usd_minor") != 2100 {
|
||||||
t.Fatalf("P0 metrics mismatch: %+v", newUsers.Total.Metrics)
|
t.Fatalf("P0 metrics mismatch: %+v", newUsers.Total.Metrics)
|
||||||
}
|
}
|
||||||
|
profileSetupColumnFound := false
|
||||||
|
for _, column := range newUsers.Columns {
|
||||||
|
if column.Key == "profile_setup_view_users" {
|
||||||
|
profileSetupColumnFound = column.Label == "填写资料页曝光人数" && column.Type == "count"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !profileSetupColumnFound {
|
||||||
|
t.Fatalf("P0 profile setup view column contract is missing: %+v", newUsers.Columns)
|
||||||
|
}
|
||||||
|
|
||||||
revenue := socialTestSection(t, result, SocialSectionRevenue)
|
revenue := socialTestSection(t, result, SocialSectionRevenue)
|
||||||
// 文档口径:总充值 2100 = Google 500 + 三方(MiFaPay) 700 + 币商(finance verified) 900。
|
// 文档口径:总充值 2100 = Google 500 + 三方(MiFaPay) 700 + 币商(finance verified) 900。
|
||||||
|
|||||||
@ -195,7 +195,8 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
app_code VARCHAR(32) NOT NULL, stat_tz VARCHAR(64) NOT NULL DEFAULT 'UTC', stat_day DATE NOT NULL,
|
app_code VARCHAR(32) NOT NULL, stat_tz VARCHAR(64) NOT NULL DEFAULT 'UTC', stat_day DATE NOT NULL,
|
||||||
subject_key VARCHAR(160) NOT NULL, user_id BIGINT NOT NULL DEFAULT 0, device_id VARCHAR(128) NOT NULL DEFAULT '',
|
subject_key VARCHAR(160) NOT NULL, user_id BIGINT NOT NULL DEFAULT 0, device_id VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
country_id BIGINT NOT NULL DEFAULT 0, region_id BIGINT NOT NULL DEFAULT 0, user_role VARCHAR(16) NOT NULL DEFAULT 'user',
|
country_id BIGINT NOT NULL DEFAULT 0, region_id BIGINT NOT NULL DEFAULT 0, user_role VARCHAR(16) NOT NULL DEFAULT 'user',
|
||||||
app_first_open TINYINT(1) NOT NULL DEFAULT 0, registered_success TINYINT(1) NOT NULL DEFAULT 0,
|
app_first_open TINYINT(1) NOT NULL DEFAULT 0, profile_setup_view TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
|
registered_success TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
active_user TINYINT(1) NOT NULL DEFAULT 0, room_join_success TINYINT(1) NOT NULL DEFAULT 0, room_join_fail TINYINT(1) NOT NULL DEFAULT 0,
|
active_user TINYINT(1) NOT NULL DEFAULT 0, room_join_success TINYINT(1) NOT NULL DEFAULT 0, room_join_fail TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
mic_up_success TINYINT(1) NOT NULL DEFAULT 0, gift_sent TINYINT(1) NOT NULL DEFAULT 0, normal_gift_sent TINYINT(1) NOT NULL DEFAULT 0,
|
mic_up_success TINYINT(1) NOT NULL DEFAULT 0, gift_sent TINYINT(1) NOT NULL DEFAULT 0, normal_gift_sent TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
lucky_gift_sent TINYINT(1) NOT NULL DEFAULT 0, super_lucky_gift_sent TINYINT(1) NOT NULL DEFAULT 0,
|
lucky_gift_sent TINYINT(1) NOT NULL DEFAULT 0, super_lucky_gift_sent TINYINT(1) NOT NULL DEFAULT 0,
|
||||||
@ -431,6 +432,8 @@ func (r *Repository) Migrate(ctx context.Context) error {
|
|||||||
`ALTER TABLE stat_app_day_country ADD COLUMN mic_online_users BIGINT NOT NULL DEFAULT 0 AFTER mic_online_ms`,
|
`ALTER TABLE stat_app_day_country ADD COLUMN mic_online_users BIGINT NOT NULL DEFAULT 0 AFTER mic_online_ms`,
|
||||||
`ALTER TABLE stat_game_day_country ADD COLUMN refund_coin BIGINT NOT NULL DEFAULT 0 AFTER payout_coin`,
|
`ALTER TABLE stat_game_day_country ADD COLUMN refund_coin BIGINT NOT NULL DEFAULT 0 AFTER payout_coin`,
|
||||||
`ALTER TABLE stat_social_user_day ADD COLUMN room_online_ms BIGINT NOT NULL DEFAULT 0 AFTER room_stay_ms`,
|
`ALTER TABLE stat_social_user_day ADD COLUMN room_online_ms BIGINT NOT NULL DEFAULT 0 AFTER room_stay_ms`,
|
||||||
|
// 单个带默认值的标记列使用 INSTANT 元数据变更,不在服务启动时重写 Social 用户日宽表。
|
||||||
|
`ALTER TABLE stat_social_user_day ADD COLUMN profile_setup_view TINYINT(1) NOT NULL DEFAULT 0 AFTER app_first_open, ALGORITHM=INSTANT`,
|
||||||
`ALTER TABLE stat_app_day_country ADD COLUMN region_id BIGINT NOT NULL DEFAULT 0 AFTER country_id`,
|
`ALTER TABLE stat_app_day_country ADD COLUMN region_id BIGINT NOT NULL DEFAULT 0 AFTER country_id`,
|
||||||
`ALTER TABLE stat_user_day_activity ADD COLUMN region_id BIGINT NOT NULL DEFAULT 0 AFTER country_id`,
|
`ALTER TABLE stat_user_day_activity ADD COLUMN region_id BIGINT NOT NULL DEFAULT 0 AFTER country_id`,
|
||||||
`ALTER TABLE stat_user_registration ADD COLUMN region_id BIGINT NOT NULL DEFAULT 0 AFTER country_id`,
|
`ALTER TABLE stat_user_registration ADD COLUMN region_id BIGINT NOT NULL DEFAULT 0 AFTER country_id`,
|
||||||
|
|||||||
@ -107,6 +107,7 @@ type socialRequirementUserDay struct {
|
|||||||
RegionID int64
|
RegionID int64
|
||||||
UserRole string
|
UserRole string
|
||||||
AppFirstOpen int64
|
AppFirstOpen int64
|
||||||
|
ProfileSetupView int64
|
||||||
Registered int64
|
Registered int64
|
||||||
Active int64
|
Active int64
|
||||||
RoomJoinOK int64
|
RoomJoinOK int64
|
||||||
@ -155,6 +156,7 @@ type socialRequirementUserDay struct {
|
|||||||
|
|
||||||
type socialRequirementAggregate struct {
|
type socialRequirementAggregate struct {
|
||||||
AppFirstOpen int64
|
AppFirstOpen int64
|
||||||
|
ProfileSetupView int64
|
||||||
Registered int64
|
Registered int64
|
||||||
NewHostUsers int64
|
NewHostUsers int64
|
||||||
NewNormalUsers int64
|
NewNormalUsers int64
|
||||||
@ -385,7 +387,7 @@ func (r *Repository) querySocialUserDays(ctx context.Context, app string, statTZ
|
|||||||
// 过滤会丢掉同一用户登录前的活跃,并让区域 DAU 再次出现 d/u 口径偏差。
|
// 过滤会丢掉同一用户登录前的活跃,并让区域 DAU 再次出现 d/u 口径偏差。
|
||||||
rows, err := r.db.QueryContext(ctx, `
|
rows, err := r.db.QueryContext(ctx, `
|
||||||
SELECT DATE_FORMAT(stat_day, '%Y-%m-%d'), subject_key, user_id, device_id, country_id, region_id, user_role,
|
SELECT DATE_FORMAT(stat_day, '%Y-%m-%d'), subject_key, user_id, device_id, country_id, region_id, user_role,
|
||||||
app_first_open, registered_success, active_user, room_join_success, room_join_fail,
|
app_first_open, profile_setup_view, registered_success, active_user, room_join_success, room_join_fail,
|
||||||
mic_up_success, gift_sent, normal_gift_sent, lucky_gift_sent, super_lucky_gift_sent,
|
mic_up_success, gift_sent, normal_gift_sent, lucky_gift_sent, super_lucky_gift_sent,
|
||||||
recharge_user, first_recharge_user, repeat_recharge_user, game_bet_user, self_game_bet_user,
|
recharge_user, first_recharge_user, repeat_recharge_user, game_bet_user, self_game_bet_user,
|
||||||
probability_game_bet_user, chat_sent, follow_user, follow_room, gift_panel_open, probability_game_panel_open,
|
probability_game_bet_user, chat_sent, follow_user, follow_room, gift_panel_open, probability_game_panel_open,
|
||||||
@ -407,7 +409,7 @@ func (r *Repository) querySocialUserDays(ctx context.Context, app string, statTZ
|
|||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var item socialRequirementUserDay
|
var item socialRequirementUserDay
|
||||||
if err := rows.Scan(&item.StatDay, &item.Subject, &item.UserID, &item.DeviceID, &item.CountryID, &item.RegionID, &item.UserRole,
|
if err := rows.Scan(&item.StatDay, &item.Subject, &item.UserID, &item.DeviceID, &item.CountryID, &item.RegionID, &item.UserRole,
|
||||||
&item.AppFirstOpen, &item.Registered, &item.Active, &item.RoomJoinOK, &item.RoomJoinNG,
|
&item.AppFirstOpen, &item.ProfileSetupView, &item.Registered, &item.Active, &item.RoomJoinOK, &item.RoomJoinNG,
|
||||||
&item.MicUp, &item.Gift, &item.NormalGift, &item.LuckyGift, &item.SuperGift,
|
&item.MicUp, &item.Gift, &item.NormalGift, &item.LuckyGift, &item.SuperGift,
|
||||||
&item.Recharge, &item.FirstRecharge, &item.RepeatRecharge, &item.GameBet, &item.SelfGameBet,
|
&item.Recharge, &item.FirstRecharge, &item.RepeatRecharge, &item.GameBet, &item.SelfGameBet,
|
||||||
&item.ProbabilityBet, &item.Chat, &item.FollowUser, &item.FollowRoom, &item.GiftPanel, &item.ProbabilityPanel,
|
&item.ProbabilityBet, &item.Chat, &item.FollowUser, &item.FollowRoom, &item.GiftPanel, &item.ProbabilityPanel,
|
||||||
@ -682,6 +684,7 @@ func mergeSocialRequirementUserDay(current, next socialRequirementUserDay) socia
|
|||||||
// canonicalizeSocialUserDays replaces this flag with the distinct known
|
// canonicalizeSocialUserDays replaces this flag with the distinct known
|
||||||
// device count after all rows merge; MAX is only the empty-device fallback.
|
// device count after all rows merge; MAX is only the empty-device fallback.
|
||||||
current.AppFirstOpen = maxSocialFlag(current.AppFirstOpen, next.AppFirstOpen)
|
current.AppFirstOpen = maxSocialFlag(current.AppFirstOpen, next.AppFirstOpen)
|
||||||
|
current.ProfileSetupView = maxSocialFlag(current.ProfileSetupView, next.ProfileSetupView)
|
||||||
current.Registered = maxSocialFlag(current.Registered, next.Registered)
|
current.Registered = maxSocialFlag(current.Registered, next.Registered)
|
||||||
current.Active = maxSocialFlag(current.Active, next.Active)
|
current.Active = maxSocialFlag(current.Active, next.Active)
|
||||||
current.RoomJoinOK = maxSocialFlag(current.RoomJoinOK, next.RoomJoinOK)
|
current.RoomJoinOK = maxSocialFlag(current.RoomJoinOK, next.RoomJoinOK)
|
||||||
@ -786,6 +789,9 @@ func aggregateSocialRows(rows []socialRequirementUserDay, section string, role s
|
|||||||
// 下载/首次打开是设备口径;身份归并只消除用户行为的 d/u 重复,
|
// 下载/首次打开是设备口径;身份归并只消除用户行为的 d/u 重复,
|
||||||
// 不能把同一账号在两台设备上的两次安装压成 1。
|
// 不能把同一账号在两台设备上的两次安装压成 1。
|
||||||
agg.AppFirstOpen += normalizeID(row.AppFirstOpen)
|
agg.AppFirstOpen += normalizeID(row.AppFirstOpen)
|
||||||
|
// 资料页曝光按 canonical 主体日去重:登录用户按 user_id,注册前匿名流程按 device_id;
|
||||||
|
// 同一客户端会话完成登录后,查询阶段会把 d:/u: 两行合并,避免一次曝光重复计数。
|
||||||
|
agg.ProfileSetupView += flagValue(row.ProfileSetupView)
|
||||||
agg.Registered += flagValue(row.Registered)
|
agg.Registered += flagValue(row.Registered)
|
||||||
if flagValue(row.Registered) > 0 && normalizeSocialUserRole(row.UserRole) == "host" {
|
if flagValue(row.Registered) > 0 && normalizeSocialUserRole(row.UserRole) == "host" {
|
||||||
agg.NewHostUsers++
|
agg.NewHostUsers++
|
||||||
@ -1118,6 +1124,7 @@ func socialSectionMetrics(section string, agg socialRequirementAggregate, retent
|
|||||||
default:
|
default:
|
||||||
out := map[string]any{
|
out := map[string]any{
|
||||||
"app_download_users": agg.AppFirstOpen,
|
"app_download_users": agg.AppFirstOpen,
|
||||||
|
"profile_setup_view_users": agg.ProfileSetupView,
|
||||||
"registered_users": agg.Registered,
|
"registered_users": agg.Registered,
|
||||||
"new_host_users": agg.NewHostUsers,
|
"new_host_users": agg.NewHostUsers,
|
||||||
"new_normal_users": agg.NewNormalUsers,
|
"new_normal_users": agg.NewNormalUsers,
|
||||||
@ -1312,6 +1319,7 @@ func socialRequirementMetricDefinitions(section string) []MetricDefinition {
|
|||||||
default:
|
default:
|
||||||
return []MetricDefinition{
|
return []MetricDefinition{
|
||||||
{Metric: "app_download_users", Definition: "客户端 app_first_open 去重人数;不代表应用商店真实下载量。"},
|
{Metric: "app_download_users", Definition: "客户端 app_first_open 去重人数;不代表应用商店真实下载量。"},
|
||||||
|
{Metric: "profile_setup_view_users", Definition: "进入填写资料页的新用户人数:客户端 profile_start 事件按 canonical 用户或设备主体、统计自然日去重。"},
|
||||||
// registered_users 保留历史字段兼容,业务展示统一为“新增用户”;资格和幂等边界由 UserRegistered 生产、消费链路保证。
|
// registered_users 保留历史字段兼容,业务展示统一为“新增用户”;资格和幂等边界由 UserRegistered 生产、消费链路保证。
|
||||||
{Metric: "registered_users", Definition: "注册成功的新用户人数:仅统计资料完成后由 user-service 发布的 UserRegistered 服务端事实,按用户去重;未完成资料、quick_account、game_robot 不计,MQ 重投不重复。"},
|
{Metric: "registered_users", Definition: "注册成功的新用户人数:仅统计资料完成后由 user-service 发布的 UserRegistered 服务端事实,按用户去重;未完成资料、quick_account、game_robot 不计,MQ 重投不重复。"},
|
||||||
{Metric: "new_user_d1_retention_rate", Definition: "新普通用户次日活跃留存。"},
|
{Metric: "new_user_d1_retention_rate", Definition: "新普通用户次日活跃留存。"},
|
||||||
@ -1422,6 +1430,7 @@ func socialRequirementColumns(section string) []SocialRequirementColumn {
|
|||||||
default:
|
default:
|
||||||
return []SocialRequirementColumn{
|
return []SocialRequirementColumn{
|
||||||
{Key: "app_download_users", Label: "下载APP人数", Type: "count", Tooltip: "客户端 app_first_open 去重人数"},
|
{Key: "app_download_users", Label: "下载APP人数", Type: "count", Tooltip: "客户端 app_first_open 去重人数"},
|
||||||
|
{Key: "profile_setup_view_users", Label: "填写资料页曝光人数", Type: "count", Tooltip: "进入填写资料页的新用户人数;同一用户或设备同一天重复进入只计一次"},
|
||||||
{Key: "registered_users", Label: "新增用户", Type: "count", Tooltip: "注册成功的新用户人数:资料完成后产生 UserRegistered 服务端事实并按用户去重;未完成资料、quick_account、game_robot 不计,MQ 重投不重复"},
|
{Key: "registered_users", Label: "新增用户", Type: "count", Tooltip: "注册成功的新用户人数:资料完成后产生 UserRegistered 服务端事实并按用户去重;未完成资料、quick_account、game_robot 不计,MQ 重投不重复"},
|
||||||
{Key: "new_host_users", Label: "新主播用户", Type: "count"},
|
{Key: "new_host_users", Label: "新主播用户", Type: "count"},
|
||||||
{Key: "new_normal_users", Label: "新普通用户", Type: "count"},
|
{Key: "new_normal_users", Label: "新普通用户", Type: "count"},
|
||||||
|
|||||||
@ -20,6 +20,7 @@ type socialUserDayDelta struct {
|
|||||||
UserRole string
|
UserRole string
|
||||||
UpdatedAt int64
|
UpdatedAt int64
|
||||||
FirstOpen int64
|
FirstOpen int64
|
||||||
|
ProfileSetupView int64
|
||||||
Registered int64
|
Registered int64
|
||||||
Active int64
|
Active int64
|
||||||
RoomJoinOK int64
|
RoomJoinOK int64
|
||||||
@ -99,6 +100,7 @@ func applySocialUserDayDelta(ctx context.Context, tx *sql.Tx, delta socialUserDa
|
|||||||
_, err := tx.ExecContext(ctx, `
|
_, err := tx.ExecContext(ctx, `
|
||||||
UPDATE stat_social_user_day
|
UPDATE stat_social_user_day
|
||||||
SET app_first_open = GREATEST(app_first_open, ?),
|
SET app_first_open = GREATEST(app_first_open, ?),
|
||||||
|
profile_setup_view = GREATEST(profile_setup_view, ?),
|
||||||
registered_success = GREATEST(registered_success, ?),
|
registered_success = GREATEST(registered_success, ?),
|
||||||
active_user = GREATEST(active_user, ?),
|
active_user = GREATEST(active_user, ?),
|
||||||
room_join_success = GREATEST(room_join_success, ?),
|
room_join_success = GREATEST(room_join_success, ?),
|
||||||
@ -145,7 +147,7 @@ func applySocialUserDayDelta(ctx context.Context, tx *sql.Tx, delta socialUserDa
|
|||||||
output_coin = output_coin + ?,
|
output_coin = output_coin + ?,
|
||||||
updated_at_ms = ?
|
updated_at_ms = ?
|
||||||
WHERE app_code = ? AND stat_tz = ? AND stat_day = ? AND subject_key = ?
|
WHERE app_code = ? AND stat_tz = ? AND stat_day = ? AND subject_key = ?
|
||||||
`, flagValue(delta.FirstOpen), flagValue(delta.Registered), flagValue(delta.Active), flagValue(delta.RoomJoinOK), flagValue(delta.RoomJoinNG),
|
`, flagValue(delta.FirstOpen), flagValue(delta.ProfileSetupView), flagValue(delta.Registered), flagValue(delta.Active), flagValue(delta.RoomJoinOK), flagValue(delta.RoomJoinNG),
|
||||||
flagValue(delta.MicUp), flagValue(delta.Gift), flagValue(delta.NormalGift), flagValue(delta.LuckyGift), flagValue(delta.SuperGift),
|
flagValue(delta.MicUp), flagValue(delta.Gift), flagValue(delta.NormalGift), flagValue(delta.LuckyGift), flagValue(delta.SuperGift),
|
||||||
flagValue(delta.Recharge), flagValue(delta.FirstRecharge), flagValue(delta.RepeatRecharge), flagValue(delta.GameBet), flagValue(delta.SelfGameBet),
|
flagValue(delta.Recharge), flagValue(delta.FirstRecharge), flagValue(delta.RepeatRecharge), flagValue(delta.GameBet), flagValue(delta.SelfGameBet),
|
||||||
flagValue(delta.ProbabilityBet), flagValue(delta.Chat), flagValue(delta.FollowUser), flagValue(delta.FollowRoom), flagValue(delta.GiftPanel),
|
flagValue(delta.ProbabilityBet), flagValue(delta.Chat), flagValue(delta.FollowUser), flagValue(delta.FollowRoom), flagValue(delta.GiftPanel),
|
||||||
@ -216,6 +218,10 @@ func socialDeltaForAppTrackingEvent(event AppTrackingEvent, scope statDayScope,
|
|||||||
case "app_first_open":
|
case "app_first_open":
|
||||||
delta.FirstOpen = 1
|
delta.FirstOpen = 1
|
||||||
delta.Active = 1
|
delta.Active = 1
|
||||||
|
case "profile_start":
|
||||||
|
// Flutter 仅在未完成资料的新用户真正进入 profile_setup 页面后上报;
|
||||||
|
// 用户退出重进可能重复上报,因此读模型只保留主体日级曝光标记。
|
||||||
|
delta.ProfileSetupView = 1
|
||||||
case "home_view":
|
case "home_view":
|
||||||
delta.Active = 1
|
delta.Active = 1
|
||||||
case "app_session":
|
case "app_session":
|
||||||
|
|||||||
@ -71,6 +71,8 @@ type RedPacketCreateCommand struct {
|
|||||||
PacketCount int32
|
PacketCount int32
|
||||||
// RoomLocked 是 gateway 从 room-service snapshot 读取的展示快照,只随红包资金事实透传。
|
// RoomLocked 是 gateway 从 room-service snapshot 读取的展示快照,只随红包资金事实透传。
|
||||||
RoomLocked bool
|
RoomLocked bool
|
||||||
|
// OwnerUserID 与 RoomLocked 来自同一份房间快照;wallet 不解释房主权限,只随已提交红包事实透传。
|
||||||
|
OwnerUserID int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type RedPacketCreateReceipt struct {
|
type RedPacketCreateReceipt struct {
|
||||||
|
|||||||
@ -27,6 +27,7 @@ type redPacketMetadata struct {
|
|||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
RegionID int64 `json:"region_id"`
|
RegionID int64 `json:"region_id"`
|
||||||
RoomLocked bool `json:"room_locked"`
|
RoomLocked bool `json:"room_locked"`
|
||||||
|
OwnerUserID int64 `json:"owner_user_id,omitempty"`
|
||||||
PacketType string `json:"packet_type"`
|
PacketType string `json:"packet_type"`
|
||||||
TotalAmount int64 `json:"total_amount"`
|
TotalAmount int64 `json:"total_amount"`
|
||||||
PacketCount int32 `json:"packet_count"`
|
PacketCount int32 `json:"packet_count"`
|
||||||
|
|||||||
@ -90,6 +90,7 @@ func (r *Repository) CreateRedPacket(ctx context.Context, command ledger.RedPack
|
|||||||
RoomID: command.RoomID,
|
RoomID: command.RoomID,
|
||||||
RegionID: command.RegionID,
|
RegionID: command.RegionID,
|
||||||
RoomLocked: command.RoomLocked,
|
RoomLocked: command.RoomLocked,
|
||||||
|
OwnerUserID: command.OwnerUserID,
|
||||||
PacketType: command.PacketType,
|
PacketType: command.PacketType,
|
||||||
TotalAmount: command.TotalAmount,
|
TotalAmount: command.TotalAmount,
|
||||||
PacketCount: command.PacketCount,
|
PacketCount: command.PacketCount,
|
||||||
@ -228,6 +229,7 @@ func redPacketCreatedEvent(transactionID string, commandID string, metadata redP
|
|||||||
"room_id": metadata.RoomID,
|
"room_id": metadata.RoomID,
|
||||||
"region_id": metadata.RegionID,
|
"region_id": metadata.RegionID,
|
||||||
"room_locked": metadata.RoomLocked,
|
"room_locked": metadata.RoomLocked,
|
||||||
|
"owner_user_id": metadata.OwnerUserID,
|
||||||
"total_amount": metadata.TotalAmount,
|
"total_amount": metadata.TotalAmount,
|
||||||
"packet_count": metadata.PacketCount,
|
"packet_count": metadata.PacketCount,
|
||||||
"remaining_amount": metadata.RemainingAmount,
|
"remaining_amount": metadata.RemainingAmount,
|
||||||
@ -242,7 +244,7 @@ func redPacketCreatedEvent(transactionID string, commandID string, metadata redP
|
|||||||
}
|
}
|
||||||
|
|
||||||
func redPacketCreateRequestHash(command ledger.RedPacketCreateCommand) string {
|
func redPacketCreateRequestHash(command ledger.RedPacketCreateCommand) string {
|
||||||
// room_locked 是非资金展示快照,不进入幂等冲突哈希:未知结果重试时房间可能已改密,首笔已提交事实仍应原样返回。
|
// room_locked/owner_user_id 都是非资金展示快照,不进入幂等冲突哈希:未知结果重试必须返回首笔已提交事实,不能因展示字段变化制造账务冲突。
|
||||||
return stableHash(fmt.Sprintf("red_packet_create|%s|%s|%d|%s|%d|%s|%d|%d",
|
return stableHash(fmt.Sprintf("red_packet_create|%s|%s|%d|%s|%d|%s|%d|%d",
|
||||||
appcode.Normalize(command.AppCode),
|
appcode.Normalize(command.AppCode),
|
||||||
command.CommandID,
|
command.CommandID,
|
||||||
|
|||||||
@ -14,6 +14,7 @@ func TestRedPacketCreatedEventCarriesDisplayFacts(t *testing.T) {
|
|||||||
RoomID: "9001",
|
RoomID: "9001",
|
||||||
RegionID: 310,
|
RegionID: 310,
|
||||||
RoomLocked: true,
|
RoomLocked: true,
|
||||||
|
OwnerUserID: 323411852102995968,
|
||||||
TotalAmount: 15000,
|
TotalAmount: 15000,
|
||||||
PacketCount: 5,
|
PacketCount: 5,
|
||||||
RemainingAmount: 15000,
|
RemainingAmount: 15000,
|
||||||
@ -37,4 +38,7 @@ func TestRedPacketCreatedEventCarriesDisplayFacts(t *testing.T) {
|
|||||||
if payload["room_locked"] != true {
|
if payload["room_locked"] != true {
|
||||||
t.Fatalf("red packet created payload must carry room lock snapshot: %+v", payload)
|
t.Fatalf("red packet created payload must carry room lock snapshot: %+v", payload)
|
||||||
}
|
}
|
||||||
|
if payload["owner_user_id"] != int64(323411852102995968) {
|
||||||
|
t.Fatalf("red packet created payload must carry owner snapshot: %+v", payload)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,6 +56,7 @@ func (s *Server) CreateRedPacket(ctx context.Context, req *walletv1.CreateRedPac
|
|||||||
TotalAmount: req.GetTotalAmount(),
|
TotalAmount: req.GetTotalAmount(),
|
||||||
PacketCount: req.GetPacketCount(),
|
PacketCount: req.GetPacketCount(),
|
||||||
RoomLocked: req.GetRoomLocked(),
|
RoomLocked: req.GetRoomLocked(),
|
||||||
|
OwnerUserID: req.GetOwnerUserId(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, xerr.ToGRPCError(err)
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user