// Package command 定义 room-service 可持久化、可回放的房间命令模型。
package command

import (
	"encoding/json"
	"fmt"

	"hyapp/services/room-service/internal/room/state"
)

// 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"`
	// DeviceID 是 user-service 绑定到 auth_session 并经 JWT 验签的设备 ID。
	// 旧 command log 没有该字段时反序列化为空，fixed_v2 仍可重放，dynamic_v3 由抽奖 owner fail-close。
	DeviceID string `json:"device_id,omitempty"`
	// 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"`
	// OwnerCountryCode 是创建瞬间房主所属国家，列表国家筛选以该投影为准。
	OwnerCountryCode string `json:"owner_country_code,omitempty"`
	// RoomName 是房间展示名称，恢复时回填 RoomExt["title"]，不拆成独立状态 owner。
	RoomName string `json:"room_name"`
	// RoomAvatar 是创建时必填的房间封面，恢复时回填 RoomExt["cover_url"]。
	RoomAvatar string `json:"room_avatar"`
	// CoverUploadID 固化封面 URL 的来源凭证，使同 command_id 重试不能换用另一份预上传文件。
	CoverUploadID string `json:"cover_upload_id,omitempty"`
	// RoomDescription 是房间简介，恢复时回填 RoomExt["description"]。
	RoomDescription string `json:"room_description"`
	// RoomShortID 是房间短 ID，首版等于创建者当前展示短号。
	RoomShortID string `json:"room_short_id"`
	// RobotRoom 标记该房间由内部机器人编排器创建，机器人礼物校验依赖它 fail-close。
	RobotRoom bool `json:"robot_room,omitempty"`
	// RobotUserIDs 是该机器人房间允许参与虚拟上麦和机器人送礼的用户集合。
	RobotUserIDs []int64 `json:"robot_user_ids,omitempty"`
}

// Type 返回命令类型。
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"`
	// CoverUploadID 非空表示 RoomAvatar 来自一次性预上传记录，提交事务必须同步消费。
	CoverUploadID string `json:"cover_upload_id,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" }

// SetRoomBackground 定义房主把已保存背景图设置为当前房间背景的请求。
type SetRoomBackground struct {
	Base
	// BackgroundID 必须归属于当前房间，避免房主把其他房间的素材串用到自己的房间状态。
	BackgroundID int64 `json:"background_id"`
	// BackgroundURL 是提交前从房间背景素材表解析出的确定性快照，恢复时不再回查列表表。
	BackgroundURL string `json:"background_url"`
	// Background 保存完整媒体事实；旧 command log 缺字段时恢复继续使用 BackgroundID/BackgroundURL。
	Background state.RoomBackground `json:"background,omitempty"`
}

func (SetRoomBackground) Type() string { return "set_room_background" }

// ApplyRoomDecoration 定义房主把有效 wallet entitlement 应用到指定房间的请求。
type ApplyRoomDecoration struct {
	Base
	// DecorationType 是客户端动作语义，只接受 room_border/colored_room_name。
	DecorationType string `json:"decoration_type"`
	// ResourceID/EntitlementID 是幂等载荷；Resource 是 wallet 校验后固化的恢复快照。
	ResourceID    int64                `json:"resource_id"`
	EntitlementID string               `json:"entitlement_id,omitempty"`
	Resource      state.RoomDecoration `json:"resource,omitempty"`
}

func (ApplyRoomDecoration) Type() string { return "apply_room_decoration" }

// ReconcileRoomDecoration 由 wallet owner 事实触发，用当前 benefit/resource/entitlement 状态更新或清除房间装扮。
type ReconcileRoomDecoration struct {
	Base
	DecorationType     string               `json:"decoration_type"`
	ExpectedResourceID int64                `json:"expected_resource_id"`
	BenefitCode        string               `json:"benefit_code"`
	SourceEventID      string               `json:"source_event_id"`
	Active             bool                 `json:"active"`
	Resource           state.RoomDecoration `json:"resource,omitempty"`
}

func (ReconcileRoomDecoration) Type() string { return "reconcile_room_decoration" }

// SendRoomImage 定义由 room-service 权限 owner 接受并写 outbox 的图片消息。
// 图片本身已由 gateway 专用上传入口解析，命令仍会复核归属、房态、禁言和 VIP 权益。
type SendRoomImage struct {
	Base
	MessageID string          `json:"message_id"`
	Image     state.RoomMedia `json:"image"`
}

func (SendRoomImage) Type() string { return "send_room_image" }

// JoinRoom 定义业务进房请求。
type EntryVehicleSnapshot struct {
	ResourceID    int64  `json:"resource_id,omitempty"`
	ResourceCode  string `json:"resource_code,omitempty"`
	Name          string `json:"name,omitempty"`
	AssetURL      string `json:"asset_url,omitempty"`
	PreviewURL    string `json:"preview_url,omitempty"`
	AnimationURL  string `json:"animation_url,omitempty"`
	MetadataJSON  string `json:"metadata_json,omitempty"`
	EntitlementID string `json:"entitlement_id,omitempty"`
	ExpiresAtMS   int64  `json:"expires_at_ms,omitempty"`
}

type JoinRoom struct {
	Base
	// Role 是用户在房间内的轻量角色，默认 audience。
	Role string `json:"role"`
	// ActorCountryID 是 gateway 从 user-service 注入的真实国家，用于统计进房活跃国家维度。
	ActorCountryID int64 `json:"actor_country_id,omitempty"`
	// ActorRegionID 是 gateway 从 user-service 注入的真实区域；房间 visible_region_id 不能代替用户国家。
	ActorRegionID int64 `json:"actor_region_id,omitempty"`
	// ActorIsRobot 标记本次进房由房间机器人调度触发，统计消费者据此排除活跃用户口径。
	ActorIsRobot bool `json:"actor_is_robot,omitempty"`
	// EntryVehicle 是 gateway 在进房时解析出的当前有效佩戴座驾快照，用于进房 IM。
	EntryVehicle EntryVehicleSnapshot `json:"entry_vehicle,omitempty"`
	// ActorDisplayProfile 是 gateway 在进房入口固化的展示快照；只服务 IM/UI 兜底，不参与 presence 权限和统计。
	ActorDisplayProfile UserDisplayProfile `json:"actor_display_profile,omitempty"`
	// PresenceRecovery 表示客户端会话仍在，只需恢复被 stale worker 清理的业务 presence。
	// 该命令不能制造新的进房展示或统计事实。
	PresenceRecovery bool `json:"presence_recovery,omitempty"`
}

// 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" }

// AdminUpdateRoom 定义后台房间资料和生命周期修改请求。
type AdminUpdateRoom struct {
	Base
	Title            *string `json:"title,omitempty"`
	CoverURL         *string `json:"cover_url,omitempty"`
	Description      *string `json:"description,omitempty"`
	Mode             *string `json:"mode,omitempty"`
	Status           *string `json:"status,omitempty"`
	VisibleRegionID  *int64  `json:"visible_region_id,omitempty"`
	OwnerCountryCode *string `json:"owner_country_code,omitempty"`
	CloseReason      string  `json:"close_reason,omitempty"`
	AdminID          uint64  `json:"admin_id,omitempty"`
	AdminName        string  `json:"admin_name,omitempty"`
}

func (AdminUpdateRoom) Type() string { return "admin_update_room" }

// AdminBanRoom 定义持久封禁命令；它与普通 closed 分离，防止房主通过进房自动恢复。
type AdminBanRoom struct {
	Base
	Reason    string `json:"reason"`
	AdminID   uint64 `json:"admin_id,omitempty"`
	AdminName string `json:"admin_name,omitempty"`
}

func (AdminBanRoom) Type() string { return "admin_ban_room" }

// AdminUnbanRoom 定义人工解封命令；解封只回到 closed，不直接制造在线房间。
type AdminUnbanRoom struct {
	Base
	Reason    string `json:"reason,omitempty"`
	AdminID   uint64 `json:"admin_id,omitempty"`
	AdminName string `json:"admin_name,omitempty"`
}

func (AdminUnbanRoom) Type() string { return "admin_unban_room" }

// AdminDeleteRoom 定义后台删除房间请求；命令先经过 Room Cell 串行确认，再由 repository 清理恢复来源。
type AdminDeleteRoom struct {
	Base
	AdminID   uint64 `json:"admin_id,omitempty"`
	AdminName string `json:"admin_name,omitempty"`
}

func (AdminDeleteRoom) Type() string { return "admin_delete_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"`
	// TargetDisplayProfile 是上麦用户展示快照，只用于 IM/UI 首帧兜底，不进入 Room Cell 状态判定。
	TargetDisplayProfile UserDisplayProfile `json:"target_display_profile,omitempty"`
}

// Type 返回命令类型。
func (MicUp) Type() string { return "mic_up" }

// RobotMicUp 定义机器人虚拟静音占麦请求。
// 它不等待 RTC 发流确认，也不会被 pending_publish 超时任务释放。
type RobotMicUp struct {
	Base
	TargetUserID int64  `json:"target_user_id"`
	SeatNo       int32  `json:"seat_no"`
	MicSessionID string `json:"mic_session_id"`
}

func (RobotMicUp) Type() string { return "robot_mic_up" }

// MicDown 定义下麦请求。
type MicDown struct {
	Base
	// TargetUserID 是被下麦用户；操作者和目标用户可以不同。
	TargetUserID int64 `json:"target_user_id"`
	// MicSessionID 非空时只允许释放同一发流会话，防止旧超时任务清掉新上麦。
	MicSessionID string `json:"mic_session_id,omitempty"`
	// Reason 记录系统触发下麦原因，例如 publish_timeout；主动下麦保持为空。
	Reason string `json:"reason,omitempty"`
	// TargetDisplayProfile 是被下麦用户展示快照，只用于补偿 IM 展示字段。
	TargetDisplayProfile UserDisplayProfile `json:"target_display_profile,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"`
	// AcceptedEventTimeMS 是 room-service 接受本次客户端确认时使用的服务端时间。
	//
	// 客户端 event_time_ms 可能受设备时钟漂移影响，只能保留为原始观测时间；恢复回放和后续媒体事件
	// 排序必须使用已经被服务端 deadline 校验过的时间，避免重启后把偏快客户端时间写回状态。
	AcceptedEventTimeMS int64 `json:"accepted_event_time_ms,omitempty"`
	// Source 记录确认来源，例如 client 或 rtc_webhook。
	Source string `json:"source,omitempty"`
	// TargetDisplayProfile 是发流确认目标用户展示快照，只用于 IM/UI 兜底。
	TargetDisplayProfile UserDisplayProfile `json:"target_display_profile,omitempty"`
}

// Type 返回命令类型。
func (ConfirmMicPublishing) Type() string { return "confirm_mic_publishing" }

// MicHeartbeat 定义当前麦位会话的服务端心跳刷新请求。
type MicHeartbeat struct {
	Base
	// TargetUserID 是被刷新麦上心跳的用户；为空时服务层使用 ActorUserID。
	TargetUserID int64 `json:"target_user_id"`
	// MicSessionID 必须匹配当前麦位会话，避免旧客户端心跳续住新麦位。
	MicSessionID string `json:"mic_session_id"`
}

// Type 返回命令类型。
func (MicHeartbeat) Type() string { return "mic_heartbeat" }

// SetMicMute 定义麦克风静音状态同步请求。
type SetMicMute struct {
	Base
	// TargetUserID 是被修改麦克风静音态的麦上用户。
	TargetUserID int64 `json:"target_user_id"`
	// Muted 为 true 表示麦克风静音，false 表示取消静音。
	Muted bool `json:"muted"`
	// TargetDisplayProfile 是被静音状态变更用户展示快照，只用于 IM/UI 兜底。
	TargetDisplayProfile UserDisplayProfile `json:"target_display_profile,omitempty"`
}

// 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"`
	// TargetDisplayProfile 是被换位用户展示快照，只用于 IM/UI 首帧兜底。
	TargetDisplayProfile UserDisplayProfile `json:"target_display_profile,omitempty"`
}

// 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"`
	// DurationMS 为 0 表示永久 ban；大于 0 表示从命令提交时间起禁止进房的时长。
	DurationMS int64 `json:"duration_ms"`
}

// 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"`
	// RequestedTargetUserIDs 保留客户端本次请求的原始目标集合；batch-send 会把离房目标从 TargetUserIDs 裁掉，
	// 但幂等比较仍必须识别同一个客户端动作，不能因为提交 payload 只保存实际送达目标而把重试判成冲突。
	RequestedTargetUserIDs []int64 `json:"requested_target_user_ids,omitempty"`
	// TargetUserID 是兼容单目标字段和批量主目标；普通 SendGift 严格要求在房间内，SendGiftBatch 可先裁掉离房目标。
	TargetUserID int64 `json:"target_user_id"`
	// GiftID 是礼物配置标识，具体配置有效性当前由调用方和 wallet 链路保证。
	GiftID string `json:"gift_id"`
	// PoolID 是幸运礼物奖池标识；为空表示普通礼物或默认幸运奖池。
	PoolID string `json:"pool_id,omitempty"`
	// LuckyGiftStrategyVersion 是 lucky-gift Check 返回的 owner 规则快照；room saga 固化它只为
	// 在滚动发布期间区分 fixed_v2 兼容与 dynamic_v3 必填 wallet 事实，不参与客户端幂等语义。
	LuckyGiftStrategyVersion string `json:"lucky_gift_strategy_version,omitempty"`
	// GiftCount 是礼物数量，必须为正数。
	GiftCount int32 `json:"gift_count"`
	// EntitlementID 是背包送礼要消耗的用户礼物权益 ID；普通金币送礼为空。
	EntitlementID string `json:"entitlement_id,omitempty"`
	// Source 区分 coin/bag；为空按普通金币链路兼容旧客户端。
	Source string `json:"source,omitempty"`
	// DisplayMode 只控制客户端展示事件形态；batch 表示保留逐目标事实，但房间 IM 合并为单条多人展示消息。
	DisplayMode string `json:"display_mode,omitempty"`
	// RoomLocked 固化首次执行送礼命令时的 Room Cell 锁状态。指针用于区分历史 saga 未保存该字段与明确的 false。
	RoomLocked *bool `json:"room_locked,omitempty"`
	// ComboSessionID/BatchSeq 描述 Flutter 微批顺序；它们进入 command_id 载荷冲突校验，
	// 但不能替代 Base.CommandID 作为钱包、抽奖和房间提交的唯一幂等键。
	ComboSessionID string `json:"combo_session_id,omitempty"`
	BatchSeq       int64  `json:"batch_seq,omitempty"`
	// SenderRegionID 是送礼用户所属区域，由 gateway 从 user-service 注入。
	SenderRegionID int64 `json:"sender_region_id,omitempty"`
	// SenderCountryID 是送礼用户所属国家，由 gateway 从 user-service 注入，统计服务不能用房间区域反推。
	SenderCountryID int64 `json:"sender_country_id,omitempty"`
	// SenderRegisteredAtMS 是 gateway 从 user-service 固化的账号创建时间。
	// room 恢复首次送礼 saga 时必须继续使用该值；旧命令缺失时保留 0，让 lucky-gift 对补偿大奖资格 fail-close。
	SenderRegisteredAtMS int64 `json:"sender_registered_at_ms,omitempty"`
	// TargetIsHost 是 gateway 从 user-service active host profile 注入的工资入账开关。
	TargetIsHost bool `json:"target_is_host,omitempty"`
	// TargetHostRegionID 是工资政策匹配区域；room-service 不自行推导主播身份或区域。
	TargetHostRegionID int64 `json:"target_host_region_id,omitempty"`
	// TargetAgencyOwnerUserID 是 gateway 从 user-service active host profile 注入的代理收款人快照。
	TargetAgencyOwnerUserID int64 `json:"target_agency_owner_user_id,omitempty"`
	// TargetHostScopes 是 gateway 为批量送礼逐接收方固化的主播工资入账快照。
	TargetHostScopes []GiftTargetHostScope `json:"target_host_scopes,omitempty"`
	// SenderDisplayProfile 是 gateway 在入口读取的送礼人展示快照；room-service 不把它写入 RoomState。
	SenderDisplayProfile GiftDisplayProfile `json:"sender_display_profile,omitempty"`
	// TargetDisplayProfiles 是 gateway 在入口读取的收礼人展示快照；按 target_user_id 对齐多目标 IM。
	TargetDisplayProfiles []GiftDisplayProfile `json:"target_display_profiles,omitempty"`
	// BillingReceiptID 是 wallet-service 成功落账后的回执，用于排障和恢复审计。
	BillingReceiptID string `json:"billing_receipt_id,omitempty"`
	// CoinSpent 是 sender COIN 实际扣减值，来自 wallet-service 服务端价格。
	CoinSpent int64 `json:"coin_spent,omitempty"`
	// GiftPointAdded 是历史命令字段；GIFT_POINT 已下线，新送礼不再赋值，恢复逻辑只依赖 HeatValue。
	GiftPointAdded int64 `json:"gift_point_added,omitempty"`
	// HeatValue 是 Room Cell 恢复时唯一可信的热度增量，不能重新用客户端价格推导。
	HeatValue int64 `json:"heat_value,omitempty"`
	// TargetGiftValues 记录每个收礼用户本次从 wallet receipt 得到的热度增量；恢复时只按目标增量重建收礼用户在线态。
	TargetGiftValues map[int64]int64 `json:"target_gift_values,omitempty"`
	// PriceVersion 是本次钱包结算使用的服务端礼物价格版本。
	PriceVersion string `json:"price_version,omitempty"`
	// GiftTypeCode 是 wallet-service 结算时锁定的礼物类型，用于火箭礼物类型燃料规则。
	GiftTypeCode string `json:"gift_type_code,omitempty"`
	// HostPeriodDiamondAdded 是 wallet-service 给主播周期钻石账户本次入账的钻石数。
	HostPeriodDiamondAdded int64 `json:"host_period_diamond_added,omitempty"`
	// HostPeriodCycleKey 是本次周期钻石入账所属 UTC 月周期。
	HostPeriodCycleKey string `json:"host_period_cycle_key,omitempty"`
	// RocketTouched 表示本次送礼已经计算过火箭状态，恢复时直接使用下列确定性快照。
	RocketTouched bool `json:"rocket_touched,omitempty"`
	// RocketAddedFuel 是按后台规则算出的理论燃料，倒计时和溢出规则会让它不完全生效。
	RocketAddedFuel int64 `json:"rocket_added_fuel,omitempty"`
	// RocketEffectiveFuel 是实际写入当前等级进度的燃料，溢出不会进入下一级。
	RocketEffectiveFuel int64 `json:"rocket_effective_fuel,omitempty"`
	// RocketOverflowFuel 是本次礼物在当前等级阈值之外被丢弃的燃料。
	RocketOverflowFuel int64 `json:"rocket_overflow_fuel,omitempty"`
	// RocketLevel/RocketFuel/RocketStatus 是送礼后火箭状态快照。
	RocketLevel  int32  `json:"rocket_level,omitempty"`
	RocketFuel   int64  `json:"rocket_fuel,omitempty"`
	RocketStatus string `json:"rocket_status,omitempty"`
	// RocketFuelThreshold 是当前等级阈值快照。
	RocketFuelThreshold int64 `json:"rocket_fuel_threshold,omitempty"`
	// RocketIgnitedAtMS/RocketLaunchAtMS 只在点火进入倒计时时写入。
	RocketIgnitedAtMS int64 `json:"rocket_ignited_at_ms,omitempty"`
	RocketLaunchAtMS  int64 `json:"rocket_launch_at_ms,omitempty"`
	// RocketResetAtMS 是该火箭进度所属 UTC 日的下一次重置时间。
	RocketResetAtMS int64 `json:"rocket_reset_at_ms,omitempty"`
	// RocketTop1UserID/RocketIgniterUserID 在燃料满瞬间锁定，离房后仍用它们发奖。
	RocketTop1UserID    int64 `json:"rocket_top1_user_id,omitempty"`
	RocketIgniterUserID int64 `json:"rocket_igniter_user_id,omitempty"`
	// RocketID 是本轮火箭幂等 ID，发射、抽奖和 IM 都围绕它关联。
	RocketID string `json:"rocket_id,omitempty"`
	// RocketConfigVersion 是本轮火箭采用的后台配置版本。
	RocketConfigVersion int64 `json:"rocket_config_version,omitempty"`
	// RocketPendingLaunches 是送礼提交后的待发射队列，恢复时不能只保存当前进度。
	RocketPendingLaunches []RocketPendingLaunch `json:"rocket_pending_launches,omitempty"`
	// RocketCurrentContributions 保存当前等级真实加燃料贡献，恢复时用于继续按本级贡献锁 top1。
	RocketCurrentContributions []RocketContribution `json:"rocket_current_contributions,omitempty"`
	// RobotGift 表示本次礼物只服务机器人房间展示；不写真实统计、任务、周星和房间贡献榜。
	RobotGift bool `json:"robot_gift,omitempty"`
	// RobotWalletGift 表示本次礼物由机器人钱包结算；真人房间机器人可以用它贡献真实房间热度。
	RobotWalletGift bool `json:"robot_wallet_gift,omitempty"`
	// SyntheticLuckyGift 表示幸运礼物只生成展示事件，不调用真实 activity 抽奖和奖池。
	SyntheticLuckyGift bool `json:"synthetic_lucky_gift,omitempty"`
	// SyntheticLuckyRewardCoins 是机器人幸运礼物展示用爆奖金币值，不代表真实钱包返奖。
	SyntheticLuckyRewardCoins int64 `json:"synthetic_lucky_reward_coins,omitempty"`
	// SyntheticLuckyMultiplierPPM 是机器人幸运礼物展示用倍率。
	SyntheticLuckyMultiplierPPM int64 `json:"synthetic_lucky_multiplier_ppm,omitempty"`
}

// Type 返回命令类型。
func (SendGift) Type() string { return "send_gift" }

// ApplyRoomRPSGift 记录房内 PK 最终结算后唯一真实支付的败者礼物。
// 它只恢复火箭状态，不重复执行钱包改账，也不冒充普通 SendGift 产生第二份礼物事实。
type ApplyRoomRPSGift struct {
	Base
	TargetUserID               int64                 `json:"target_user_id"`
	GiftID                     string                `json:"gift_id"`
	HeatValue                  int64                 `json:"heat_value"`
	RocketTouched              bool                  `json:"rocket_touched,omitempty"`
	RocketAddedFuel            int64                 `json:"rocket_added_fuel,omitempty"`
	RocketEffectiveFuel        int64                 `json:"rocket_effective_fuel,omitempty"`
	RocketOverflowFuel         int64                 `json:"rocket_overflow_fuel,omitempty"`
	RocketLevel                int32                 `json:"rocket_level,omitempty"`
	RocketFuel                 int64                 `json:"rocket_fuel,omitempty"`
	RocketStatus               string                `json:"rocket_status,omitempty"`
	RocketFuelThreshold        int64                 `json:"rocket_fuel_threshold,omitempty"`
	RocketIgnitedAtMS          int64                 `json:"rocket_ignited_at_ms,omitempty"`
	RocketLaunchAtMS           int64                 `json:"rocket_launch_at_ms,omitempty"`
	RocketResetAtMS            int64                 `json:"rocket_reset_at_ms,omitempty"`
	RocketTop1UserID           int64                 `json:"rocket_top1_user_id,omitempty"`
	RocketIgniterUserID        int64                 `json:"rocket_igniter_user_id,omitempty"`
	RocketID                   string                `json:"rocket_id,omitempty"`
	RocketConfigVersion        int64                 `json:"rocket_config_version,omitempty"`
	RocketPendingLaunches      []RocketPendingLaunch `json:"rocket_pending_launches,omitempty"`
	RocketCurrentContributions []RocketContribution  `json:"rocket_current_contributions,omitempty"`
}

func (ApplyRoomRPSGift) Type() string { return "apply_room_rps_gift" }

// GiftTargetHostScope 记录一个接收方在 gateway 入站时看到的主播工资入账资格。
type GiftTargetHostScope struct {
	TargetUserID            int64 `json:"target_user_id"`
	TargetIsHost            bool  `json:"target_is_host,omitempty"`
	TargetHostRegionID      int64 `json:"target_host_region_id,omitempty"`
	TargetAgencyOwnerUserID int64 `json:"target_agency_owner_user_id,omitempty"`
}

// UserDisplayProfile 是用户展示资料的轻量快照。
// 该结构只用于异步 IM 展示，避免客户端收到事件后还要等待在线成员资料刷新。
type UserDisplayProfile struct {
	// UserID 是快照归属用户；事件消费者用它确认快照没有串到其他用户。
	UserID int64 `json:"user_id"`
	// Nickname 是客户端优先展示的昵称；为空时客户端继续回退 display_user_id。
	Nickname string `json:"nickname,omitempty"`
	// Avatar 是进房瞬间头像 URL，客户端可直接渲染进场横幅头像。
	Avatar string `json:"avatar,omitempty"`
	// DisplayUserID 是当前 active 短号，昵称缺失时给客户端兜底展示。
	DisplayUserID string `json:"display_user_id,omitempty"`
	// PrettyDisplayUserID 是当前靓号展示值，保留给客户端后续展示策略选择。
	PrettyDisplayUserID string `json:"pretty_display_user_id,omitempty"`
}

// GiftDisplayProfile 是送礼入口固化的用户展示快照。
// 它只服务 IM 和历史事件展示兜底，不参与权限、账务、统计或 Room Cell 核心状态判定。
type GiftDisplayProfile struct {
	// UserID 是快照归属用户，收礼多目标时用它对齐具体 target_user_id。
	UserID int64 `json:"user_id"`
	// Username 是客户端优先展示的昵称；为空时客户端可继续回退 display_user_id。
	Username string `json:"username,omitempty"`
	// Avatar 是送礼瞬间的头像 URL，IM 飘窗可直接渲染，不再临时回查用户服务。
	Avatar string `json:"avatar,omitempty"`
	// DisplayUserID 是当前 active 短号，昵称缺失时给 Flutter 兜底展示。
	DisplayUserID string `json:"display_user_id,omitempty"`
	// PrettyDisplayUserID 是当前靓号展示值，保留给后续客户端按展示策略选择。
	PrettyDisplayUserID string `json:"pretty_display_user_id,omitempty"`
	// EffectiveVIPLevel 固化送礼入口看到的生效等级，命令重放时不能重新查询导致同一礼物事件展示漂移。
	EffectiveVIPLevel int32 `json:"effective_vip_level,omitempty"`
	// GiftBroadcastAnimationURL 固化 wallet 已校验的 gift_tray_skin；普通、幸运和超级幸运礼物统一复用该快照。
	GiftBroadcastAnimationURL string `json:"gift_broadcast_animation_url,omitempty"`
}

// RocketRewardGrant 记录发射命令中已经结算出的资源组发放结果。
type RocketRewardGrant struct {
	RewardRole      string `json:"reward_role"`
	UserID          int64  `json:"user_id"`
	RewardItemID    string `json:"reward_item_id"`
	ResourceGroupID int64  `json:"resource_group_id"`
	DisplayName     string `json:"display_name,omitempty"`
	IconURL         string `json:"icon_url,omitempty"`
	GrantID         string `json:"grant_id,omitempty"`
	Status          string `json:"status"`
}

// RocketContribution 记录当前火箭等级内的实际燃料贡献快照。
type RocketContribution struct {
	UserID      int64 `json:"user_id"`
	Fuel        int64 `json:"fuel"`
	UpdatedAtMS int64 `json:"updated_at_ms"`
}

// RocketPendingLaunch 记录一枚已点火火箭的延迟发射快照。
type RocketPendingLaunch struct {
	RocketID      string `json:"rocket_id"`
	Level         int32  `json:"level"`
	FuelThreshold int64  `json:"fuel_threshold"`
	IgnitedAtMS   int64  `json:"ignited_at_ms"`
	LaunchAtMS    int64  `json:"launch_at_ms"`
	ResetAtMS     int64  `json:"reset_at_ms"`
	Top1UserID    int64  `json:"top1_user_id,omitempty"`
	IgniterUserID int64  `json:"igniter_user_id,omitempty"`
	ConfigVersion int64  `json:"config_version"`
	CoverURL      string `json:"cover_url,omitempty"`
}

// LaunchRoomRocket 定义系统到点发射命令。
type LaunchRoomRocket struct {
	Base
	RocketID string `json:"rocket_id"`
	Level    int32  `json:"level"`
	// Current* 是发射命令提交后的当前进度快照；发射不推进当前等级，只移除待发射队列。
	CurrentLevel         int32                 `json:"current_level"`
	CurrentFuel          int64                 `json:"current_fuel"`
	CurrentFuelThreshold int64                 `json:"current_fuel_threshold"`
	CurrentStatus        string                `json:"current_status"`
	CurrentRocketID      string                `json:"current_rocket_id,omitempty"`
	NextLevel            int32                 `json:"next_level"`
	NextFuelThreshold    int64                 `json:"next_fuel_threshold"`
	ConfigVersion        int64                 `json:"config_version"`
	ResetAtMS            int64                 `json:"reset_at_ms"`
	Top1UserID           int64                 `json:"top1_user_id,omitempty"`
	IgniterUserID        int64                 `json:"igniter_user_id,omitempty"`
	LaunchedAtMS         int64                 `json:"launched_at_ms"`
	InRoomUserIDs        []int64               `json:"in_room_user_ids,omitempty"`
	CurrentContributions []RocketContribution  `json:"current_contributions,omitempty"`
	Rewards              []RocketRewardGrant   `json:"rewards,omitempty"`
	PendingLaunches      []RocketPendingLaunch `json:"pending_launches,omitempty"`
}

// Type 返回命令类型。
func (LaunchRoomRocket) Type() string { return "launch_room_rocket" }

// 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")
	// device_id 是首次请求的可信风控快照，会在 gift operation/command log 中按原值恢复；
	// 它不属于客户端业务载荷，token 旋转后的同 command_id 重试不应因此冲突。
	delete(values, "device_id")
	delete(values, "sent_at_ms")
	// 展示快照来自 gateway 对 user-service 的读取结果，不改变扣费和房间状态语义；重试同一 command_id 时不能因为昵称头像变化冲突。
	delete(values, "sender_display_profile")
	delete(values, "target_display_profiles")
	if requestedTargetUserIDs, ok := values["requested_target_user_ids"]; ok {
		// SendGiftBatch 成功提交时只把实际送达目标写入 target_user_ids；幂等比较必须回到客户端原始目标集合，
		// 否则同一 command_id 的正常重试会因为 presence 过滤结果不同而被误判为 payload conflict。
		values["target_user_ids"] = requestedTargetUserIDs
		if firstTargetUserID, exists := firstJSONNumber(requestedTargetUserIDs); exists {
			values["target_user_id"] = firstTargetUserID
		}
		delete(values, "requested_target_user_ids")
	}
	// host scope 是 gateway 入站读取的工资快照；提交后目标过滤可能裁掉部分 scope，同一 command_id 重试不应因此冲突。
	delete(values, "target_host_scopes")
	delete(values, "target_is_host")
	delete(values, "target_host_region_id")
	delete(values, "target_agency_owner_user_id")
	// sender country/region are also gateway-resolved snapshots. The first gift_operation persists
	// their exact values for settlement and statistics, but a later retry of the same client body must
	// not conflict merely because the user changed profile between timeout and replay.
	delete(values, "sender_region_id")
	delete(values, "sender_country_id")
	// 注册时间与国家/区域同属 gateway 从 user owner 解析的可信快照。首次请求会完整持久化，
	// 但它不是客户端送礼载荷；同一 command_id 重试不能因为用户快照修复或迁移而产生冲突。
	delete(values, "sender_registered_at_ms")
	// room_locked 是 Room Cell 在首次执行时补入的展示快照，不是客户端请求字段；重试不能因后续改密而触发幂等冲突。
	delete(values, "room_locked")
	// 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, "target_gift_values")
	delete(values, "price_version")
	delete(values, "gift_type_code")
	delete(values, "host_period_diamond_added")
	delete(values, "host_period_cycle_key")
	delete(values, "lucky_gift_strategy_version")
	delete(values, "rocket_touched")
	delete(values, "rocket_added_fuel")
	delete(values, "rocket_effective_fuel")
	delete(values, "rocket_overflow_fuel")
	delete(values, "rocket_level")
	delete(values, "rocket_fuel")
	delete(values, "rocket_status")
	delete(values, "rocket_fuel_threshold")
	delete(values, "rocket_ignited_at_ms")
	delete(values, "rocket_launch_at_ms")
	delete(values, "rocket_reset_at_ms")
	delete(values, "rocket_top1_user_id")
	delete(values, "rocket_igniter_user_id")
	delete(values, "rocket_id")
	delete(values, "rocket_config_version")
	delete(values, "rocket_pending_launches")
	delete(values, "rocket_current_contributions")
	// LaunchRoomRocket 的以下字段是 worker 在发射时结算出的结果，不属于触发发射的幂等语义。
	delete(values, "current_level")
	delete(values, "current_fuel")
	delete(values, "current_fuel_threshold")
	delete(values, "current_status")
	delete(values, "current_rocket_id")
	delete(values, "config_version")
	delete(values, "reset_at_ms")
	delete(values, "top1_user_id")
	delete(values, "igniter_user_id")
	delete(values, "launched_at_ms")
	delete(values, "in_room_user_ids")
	delete(values, "current_contributions")
	delete(values, "rewards")
	delete(values, "pending_launches")
	// SetRoomPassword 的哈希带随机盐；通用 payload 比较先去掉哈希，service 层再用 transient 明文和已存 bcrypt 哈希确认是否同一密码。
	delete(values, "password_hash")
	// MicUp 的发流确认 deadline 由 room-service 生成，客户端重试同一 command_id 时不能因此冲突。
	delete(values, "publish_deadline_ms")
	// ConfirmMicPublishing 的接受时间由 room-service 生成，不能参与客户端请求幂等语义。
	delete(values, "accepted_event_time_ms")
	// SetRoomBackground/ApplyRoomDecoration 的完整资源快照来自 owner 表或 wallet，不属于客户端幂等载荷。
	// 客户端只按 background_id 或 decoration_type/resource_id/entitlement_id 表达动作。
	delete(values, "background_url")
	delete(values, "background")
	delete(values, "resource")
	// 图片 message_id 由 room_id + command_id 确定性生成；真正的图片属性仍参与冲突判断。
	delete(values, "message_id")

	return json.Marshal(values)
}

func firstJSONNumber(value any) (any, bool) {
	items, ok := value.([]any)
	if !ok || len(items) == 0 {
		return nil, false
	}
	switch typed := items[0].(type) {
	case float64:
		return typed, true
	case int64:
		return typed, true
	case int:
		return typed, true
	default:
		return nil, false
	}
}

// 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 SetRoomBackground{}.Type():
		cmd = &SetRoomBackground{}
	case ApplyRoomDecoration{}.Type():
		cmd = &ApplyRoomDecoration{}
	case ReconcileRoomDecoration{}.Type():
		cmd = &ReconcileRoomDecoration{}
	case SendRoomImage{}.Type():
		cmd = &SendRoomImage{}
	case JoinRoom{}.Type():
		cmd = &JoinRoom{}
	case RoomHeartbeat{}.Type():
		cmd = &RoomHeartbeat{}
	case LeaveRoom{}.Type():
		cmd = &LeaveRoom{}
	case CloseRoom{}.Type():
		cmd = &CloseRoom{}
	case AdminUpdateRoom{}.Type():
		cmd = &AdminUpdateRoom{}
	case AdminBanRoom{}.Type():
		cmd = &AdminBanRoom{}
	case AdminUnbanRoom{}.Type():
		cmd = &AdminUnbanRoom{}
	case AdminDeleteRoom{}.Type():
		cmd = &AdminDeleteRoom{}
	case MicUp{}.Type():
		cmd = &MicUp{}
	case RobotMicUp{}.Type():
		cmd = &RobotMicUp{}
	case MicDown{}.Type():
		cmd = &MicDown{}
	case ConfirmMicPublishing{}.Type():
		cmd = &ConfirmMicPublishing{}
	case MicHeartbeat{}.Type():
		cmd = &MicHeartbeat{}
	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{}
	case ApplyRoomRPSGift{}.Type():
		cmd = &ApplyRoomRPSGift{}
	case LaunchRoomRocket{}.Type():
		cmd = &LaunchRoomRocket{}
	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
}
