// Package command 定义 room-service 可持久化、可回放的房间命令模型。 package command import ( "encoding/json" "fmt" ) // Command 统一表达可写入 command log 的房间命令。 type Command interface { // ID 返回命令幂等键,通常来自 gateway 的 RequestMeta.command_id。 ID() string // RoomID 返回命令所属房间,Room Cell 路由和 command log 分区依赖它。 RoomID() string // ActorUserID 返回命令操作者,不能由具体命令字段重复推导。 ActorUserID() int64 // Type 返回稳定命令类型,持久化和恢复反序列化都依赖该字符串。 Type() string } // Base 承载所有房间命令都共享的元信息。 type Base struct { // AppCode 是多 App 租户键;同一个 room_id 在不同 App 下必须完全隔离。 AppCode string `json:"app_code"` // RequestID 是入口请求 ID,用于追踪一次外部调用。 RequestID string `json:"request_id"` // CommandID 是幂等主键,重复命令必须返回已提交状态而不是重复变更房间。 CommandID string `json:"command_id"` // ActorID 是操作者用户 ID,房间权限和 presence 校验以它为准。 ActorID int64 `json:"actor_user_id"` // Room 是目标房间 ID,命令只允许作用于一个 Room Cell。 Room string `json:"room_id"` // GatewayNodeID 记录入口节点,便于排查命令来源。 GatewayNodeID string `json:"gateway_node_id"` // SessionID 记录 gateway 看到的客户端会话,不等同于腾讯云 IM 连接。 SessionID string `json:"session_id"` // SentAtMS 是客户端请求进入系统的时间,恢复回放时用于还原用户态时间字段。 SentAtMS int64 `json:"sent_at_ms"` } // ID 返回命令幂等主键。 func (b Base) ID() string { return b.CommandID } // RoomID 返回命令所属房间。 func (b Base) RoomID() string { return b.Room } // ActorUserID 返回命令操作者。 func (b Base) ActorUserID() int64 { return b.ActorID } // CreateRoom 定义房间创建请求。 type CreateRoom struct { Base // OwnerUserID 是房间所有者,创建后自动进入管理员集合。 OwnerUserID int64 `json:"owner_user_id"` // SeatCount 决定初始化麦位数量。 SeatCount int32 `json:"seat_count"` // Mode 记录房间模式,当前版本只保存不做复杂策略分发。 Mode string `json:"mode"` // VisibleRegionID 是创建时固定的房间列表区域,0 表示 GLOBAL。 VisibleRegionID int64 `json:"visible_region_id"` // RoomName 是房间展示名称,恢复时回填 RoomExt["title"],不拆成独立状态 owner。 RoomName string `json:"room_name"` // RoomAvatar 是房间展示头像,恢复时回填 RoomExt["cover_url"]。 RoomAvatar string `json:"room_avatar"` // RoomDescription 是房间简介,恢复时回填 RoomExt["description"]。 RoomDescription string `json:"room_description"` // RoomShortID 是房间短 ID,首版等于创建者当前展示短号。 RoomShortID string `json:"room_short_id"` } // Type 返回命令类型。 func (CreateRoom) Type() string { return "create_room" } // UpdateRoomProfile 定义房主修改房间展示资料和麦位数量的请求。 type UpdateRoomProfile struct { Base // RoomName 非 nil 表示更新房间展示名称;空字符串由服务层校验拒绝。 RoomName *string `json:"room_name,omitempty"` // RoomAvatar 非 nil 表示更新房间头像;空字符串会恢复为系统默认头像。 RoomAvatar *string `json:"room_avatar,omitempty"` // RoomDescription 非 nil 表示更新房间简介,允许清空。 RoomDescription *string `json:"room_description,omitempty"` // SeatCount 非 nil 表示调整麦位总数,值必须来自后台启用配置。 SeatCount *int32 `json:"seat_count,omitempty"` } // Type 返回命令类型。 func (UpdateRoomProfile) Type() string { return "update_room_profile" } // JoinRoom 定义业务进房请求。 type JoinRoom struct { Base // Role 是用户在房间内的轻量角色,默认 audience。 Role string `json:"role"` } // Type 返回命令类型。 func (JoinRoom) Type() string { return "join_room" } // RoomHeartbeat 定义房间业务 presence 显式心跳。 type RoomHeartbeat struct { Base } // Type 返回命令类型。 func (RoomHeartbeat) Type() string { return "room_heartbeat" } // LeaveRoom 定义业务离房请求。 type LeaveRoom struct { Base } // Type 返回命令类型。 func (LeaveRoom) Type() string { return "leave_room" } // CloseRoom 定义房间关闭请求。 type CloseRoom struct { Base // Reason 记录关闭来源,例如 closed_by_owner 或 admin_closed。 Reason string `json:"reason,omitempty"` } // Type 返回命令类型。 func (CloseRoom) Type() string { return "close_room" } // MicUp 定义上麦请求。 type MicUp struct { Base // SeatNo 是目标麦位编号,从 RoomState 初始化的麦位集合中查找。 SeatNo int32 `json:"seat_no"` // MicSessionID 是服务端为本次上麦生成的发流会话 ID,确认和超时释放都必须匹配它。 MicSessionID string `json:"mic_session_id"` // PublishDeadlineMS 是客户端必须确认 RTC 发流成功的截止时间。 PublishDeadlineMS int64 `json:"publish_deadline_ms"` } // Type 返回命令类型。 func (MicUp) Type() string { return "mic_up" } // MicDown 定义下麦请求。 type MicDown struct { Base // TargetUserID 是被下麦用户;操作者和目标用户可以不同。 TargetUserID int64 `json:"target_user_id"` // MicSessionID 非空时只允许释放同一发流会话,防止旧超时任务清掉新上麦。 MicSessionID string `json:"mic_session_id,omitempty"` // Reason 记录系统触发下麦原因,例如 publish_timeout;主动下麦保持为空。 Reason string `json:"reason,omitempty"` } // Type 返回命令类型。 func (MicDown) Type() string { return "mic_down" } // ConfirmMicPublishing 定义 RTC 发流成功确认请求。 type ConfirmMicPublishing struct { Base // TargetUserID 是被确认发布音频的用户;为空时由服务层使用 ActorUserID。 TargetUserID int64 `json:"target_user_id"` // MicSessionID 必须匹配当前麦位上的会话。 MicSessionID string `json:"mic_session_id"` // RoomVersion 是触发客户端/RTC 事件时看到的房间版本,旧版本事件会被忽略。 RoomVersion int64 `json:"room_version"` // EventTimeMS 是客户端 SDK 或 RTC webhook 的事件时间,必须晚于已接受事件。 EventTimeMS int64 `json:"event_time_ms"` // Source 记录确认来源,例如 client 或 rtc_webhook。 Source string `json:"source,omitempty"` } // Type 返回命令类型。 func (ConfirmMicPublishing) Type() string { return "confirm_mic_publishing" } // SetMicMute 定义麦克风静音状态同步请求。 type SetMicMute struct { Base // TargetUserID 是被修改麦克风静音态的麦上用户。 TargetUserID int64 `json:"target_user_id"` // Muted 为 true 表示麦克风静音,false 表示取消静音。 Muted bool `json:"muted"` } // Type 返回命令类型。 func (SetMicMute) Type() string { return "set_mic_mute" } // ApplyRTCEvent 定义由可信腾讯 RTC 服务端回调驱动的房间状态事件。 type ApplyRTCEvent struct { Base // TargetUserID 是腾讯 RTC 回调中的 UserId,必须映射回内部长 user_id。 TargetUserID int64 `json:"target_user_id"` // EventType 是 gateway 标准化后的 RTC 事件,当前为 audio_started/audio_stopped/room_exited。 EventType string `json:"event_type"` // EventTimeMS 是腾讯 RTC 回调中的 EventMsTs,用于丢弃旧事件。 EventTimeMS int64 `json:"event_time_ms"` // Reason 保留腾讯回调 reason 和入口语义,便于 IM 房间消息和审计排障。 Reason string `json:"reason,omitempty"` // Source 记录确认来源,当前为 tencent_rtc_callback。 Source string `json:"source,omitempty"` // ExternalEventID 是由 gateway 从腾讯事件字段派生的稳定事件标识。 ExternalEventID string `json:"external_event_id,omitempty"` } // Type 返回命令类型。 func (ApplyRTCEvent) Type() string { return "apply_rtc_event" } // ChangeMicSeat 定义换麦位请求。 type ChangeMicSeat struct { Base // TargetUserID 是需要换位的用户。 TargetUserID int64 `json:"target_user_id"` // SeatNo 是目标麦位编号。 SeatNo int32 `json:"seat_no"` } // Type 返回命令类型。 func (ChangeMicSeat) Type() string { return "change_mic_seat" } // SetMicSeatLock 定义锁定或解锁麦位请求。 type SetMicSeatLock struct { Base // SeatNo 是目标麦位编号。 SeatNo int32 `json:"seat_no"` // Locked 为 true 表示锁定,false 表示解锁。 Locked bool `json:"locked"` } // Type 返回命令类型。 func (SetMicSeatLock) Type() string { return "set_mic_seat_lock" } // SetChatEnabled 定义房间公屏开关请求。 type SetChatEnabled struct { Base // Enabled 为 false 时 CheckSpeakPermission 会拒绝所有普通公屏发言。 Enabled bool `json:"enabled"` } // Type 返回命令类型。 func (SetChatEnabled) Type() string { return "set_chat_enabled" } // SetRoomPassword 定义房主设置或清空入房密码的请求。 type SetRoomPassword struct { Base // Locked 为 false 表示清空密码;true 表示开启入房密码校验。 Locked bool `json:"locked"` // Password 只在当前进程内参与 command_id 冲突判断,序列化时必须省略,避免明文进入 command log。 Password string `json:"-"` // PasswordHash 是服务端生成的密码哈希,command log 不能保存客户端明文。 PasswordHash string `json:"password_hash,omitempty"` } // Type 返回命令类型。 func (SetRoomPassword) Type() string { return "set_room_password" } // SetRoomAdmin 定义添加或移除管理员请求。 type SetRoomAdmin struct { Base // TargetUserID 是被添加或移除管理员身份的用户。 TargetUserID int64 `json:"target_user_id"` // Enabled 为 true 表示添加管理员,false 表示移除管理员。 Enabled bool `json:"enabled"` } // Type 返回命令类型。 func (SetRoomAdmin) Type() string { return "set_room_admin" } // MuteUser 定义禁言请求。 type MuteUser struct { Base // TargetUserID 是被修改禁言状态的用户。 TargetUserID int64 `json:"target_user_id"` // Muted 为 true 表示禁言,false 表示解除禁言。 Muted bool `json:"muted"` } // Type 返回命令类型。 func (MuteUser) Type() string { return "mute_user" } // KickUser 定义踢人请求。 type KickUser struct { Base // TargetUserID 是被踢出并加入 ban 集合的用户。 TargetUserID int64 `json:"target_user_id"` } // Type 返回命令类型。 func (KickUser) Type() string { return "kick_user" } // SystemEvictUser 定义平台级封禁、风控或后台治理触发的房间驱逐请求。 type SystemEvictUser struct { Base // TargetUserID 是被系统驱逐的房间用户。 TargetUserID int64 `json:"target_user_id"` // OperatorUserID 是触发治理动作的后台管理员或 App 管理员,不参与房间权限判断。 OperatorUserID int64 `json:"operator_user_id"` // Reason 记录封禁、风控或后台动作来源,进入 command log 便于审计追踪。 Reason string `json:"reason"` // BanFromRoom 控制是否写入当前房间 ban 集合。 BanFromRoom bool `json:"ban_from_room"` } // Type 返回命令类型。 func (SystemEvictUser) Type() string { return "system_evict_user" } // UnbanUser 定义解除房间 ban 请求。 type UnbanUser struct { Base // TargetUserID 是被解除 ban 的用户。 TargetUserID int64 `json:"target_user_id"` } // Type 返回命令类型。 func (UnbanUser) Type() string { return "unban_user" } // SendGift 定义送礼请求。 type SendGift struct { Base // TargetType 当前只支持 user;保留字段避免后续 all_mic/all_room/couple 再改命令结构。 TargetType string `json:"target_type"` // TargetUserIDs 是新协议目标用户集合;当前 target_type=user 时只允许一个用户。 TargetUserIDs []int64 `json:"target_user_ids,omitempty"` // TargetUserID 是礼物接收方,当前版本要求接收方在房间内。 TargetUserID int64 `json:"target_user_id"` // GiftID 是礼物配置标识,具体配置有效性当前由调用方和 wallet 链路保证。 GiftID string `json:"gift_id"` // GiftCount 是礼物数量,必须为正数。 GiftCount int32 `json:"gift_count"` // BillingReceiptID 是 wallet-service 成功落账后的回执,用于排障和恢复审计。 BillingReceiptID string `json:"billing_receipt_id,omitempty"` // CoinSpent 是 sender COIN 实际扣减值,来自 wallet-service 服务端价格。 CoinSpent int64 `json:"coin_spent,omitempty"` // GiftPointAdded 是 target GIFT_POINT 实际入账值,来自 wallet-service。 GiftPointAdded int64 `json:"gift_point_added,omitempty"` // HeatValue 是 Room Cell 恢复时唯一可信的热度增量,不能重新用客户端价格推导。 HeatValue int64 `json:"heat_value,omitempty"` // PriceVersion 是本次钱包结算使用的服务端礼物价格版本。 PriceVersion string `json:"price_version,omitempty"` } // Type 返回命令类型。 func (SendGift) Type() string { return "send_gift" } // Serialize 把命令编码成 command log 持久化所需的稳定载荷。 func Serialize(cmd Command) ([]byte, error) { // 当前 command log 使用 JSON 载荷,便于排查;命令类型由独立列保存。 return json.Marshal(cmd) } // IdempotencyPayloadForCommand 生成用于 command_id 冲突判定的业务载荷。 func IdempotencyPayloadForCommand(cmd Command) ([]byte, error) { payload, err := Serialize(cmd) if err != nil { return nil, err } return IdempotencyPayload(payload) } // IdempotencyPayload 去掉请求追踪和入口节点等易变字段,只保留业务命令语义。 func IdempotencyPayload(payload []byte) ([]byte, error) { var values map[string]any if err := json.Unmarshal(payload, &values); err != nil { return nil, err } delete(values, "request_id") delete(values, "gateway_node_id") delete(values, "session_id") delete(values, "sent_at_ms") // SendGift 的以下字段由 wallet-service 结算后写入 command log,不属于客户端请求幂等语义。 delete(values, "billing_receipt_id") delete(values, "coin_spent") delete(values, "gift_point_added") delete(values, "heat_value") delete(values, "price_version") // SetRoomPassword 的哈希带随机盐;通用 payload 比较先去掉哈希,service 层再用 transient 明文和已存 bcrypt 哈希确认是否同一密码。 delete(values, "password_hash") // MicUp 的发流确认 deadline 由 room-service 生成,客户端重试同一 command_id 时不能因此冲突。 delete(values, "publish_deadline_ms") return json.Marshal(values) } // Deserialize 按命令类型恢复命令载荷。 func Deserialize(commandType string, payload []byte) (Command, error) { var cmd Command // command_type 是恢复时的分发键;未知类型表示当前代码无法保证恢复语义。 switch commandType { case CreateRoom{}.Type(): cmd = &CreateRoom{} case UpdateRoomProfile{}.Type(): cmd = &UpdateRoomProfile{} case JoinRoom{}.Type(): cmd = &JoinRoom{} case RoomHeartbeat{}.Type(): cmd = &RoomHeartbeat{} case LeaveRoom{}.Type(): cmd = &LeaveRoom{} case CloseRoom{}.Type(): cmd = &CloseRoom{} case MicUp{}.Type(): cmd = &MicUp{} case MicDown{}.Type(): cmd = &MicDown{} case ConfirmMicPublishing{}.Type(): cmd = &ConfirmMicPublishing{} case SetMicMute{}.Type(): cmd = &SetMicMute{} case ApplyRTCEvent{}.Type(): cmd = &ApplyRTCEvent{} case ChangeMicSeat{}.Type(): cmd = &ChangeMicSeat{} case SetMicSeatLock{}.Type(): cmd = &SetMicSeatLock{} case SetChatEnabled{}.Type(): cmd = &SetChatEnabled{} case SetRoomPassword{}.Type(): cmd = &SetRoomPassword{} case SetRoomAdmin{}.Type(): cmd = &SetRoomAdmin{} case MuteUser{}.Type(): cmd = &MuteUser{} case KickUser{}.Type(): cmd = &KickUser{} case SystemEvictUser{}.Type(): cmd = &SystemEvictUser{} case UnbanUser{}.Type(): cmd = &UnbanUser{} case SendGift{}.Type(): cmd = &SendGift{} default: return nil, fmt.Errorf("unknown room command type: %s", commandType) } if err := json.Unmarshal(payload, cmd); err != nil { // 单条命令反序列化失败说明 command log 损坏,恢复流程必须失败而不是继续构建错误状态。 return nil, err } return cmd, nil }