2026-05-12 19:34:47 +08:00

422 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"`
// HostUserID 是主持人,创建后也进入管理员集合。
HostUserID int64 `json:"host_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" }
// 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" }
// TransferRoomHost 定义主持人转移请求。
type TransferRoomHost struct {
Base
// TargetUserID 是新的主持人,必须在当前房间 presence 中。
TargetUserID int64 `json:"target_user_id"`
}
// Type 返回命令类型。
func (TransferRoomHost) Type() string { return "transfer_room_host" }
// 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" }
// 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")
// 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 SetRoomAdmin{}.Type():
cmd = &SetRoomAdmin{}
case TransferRoomHost{}.Type():
cmd = &TransferRoomHost{}
case MuteUser{}.Type():
cmd = &MuteUser{}
case KickUser{}.Type():
cmd = &KickUser{}
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
}