293 lines
11 KiB
Go
293 lines
11 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
|
||
roomv1 "hyapp.local/api/proto/room/v1"
|
||
"hyapp/pkg/tencentim"
|
||
"hyapp/pkg/xerr"
|
||
"hyapp/services/room-service/internal/room/command"
|
||
"hyapp/services/room-service/internal/room/outbox"
|
||
"hyapp/services/room-service/internal/room/rank"
|
||
"hyapp/services/room-service/internal/room/state"
|
||
)
|
||
|
||
// SetMicSeatLock 锁定或解锁指定麦位;首版只允许锁空麦位。
|
||
func (s *Service) SetMicSeatLock(ctx context.Context, req *roomv1.SetMicSeatLockRequest) (*roomv1.SetMicSeatLockResponse, error) {
|
||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||
// 锁麦是房间管理命令,客户端不能通过麦位字段直接绕过 Room Cell 权限矩阵。
|
||
cmd := command.SetMicSeatLock{
|
||
Base: baseFromMeta(req.GetMeta()),
|
||
SeatNo: req.GetSeatNo(),
|
||
Locked: req.GetLocked(),
|
||
}
|
||
|
||
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||
if err := requireActiveRoom(current); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if err := requireManagerPresent(current, cmd.ActorUserID()); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
index := current.SeatIndex(cmd.SeatNo)
|
||
if index < 0 {
|
||
// 只能锁定房间初始化时存在的麦位编号。
|
||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "seat does not exist")
|
||
}
|
||
if cmd.Locked && current.MicSeats[index].UserID != 0 {
|
||
// 首版不把“下麦”和“锁麦”合并成一个命令,避免客户端处理复合事件。
|
||
return mutationResult{}, nil, xerr.New(xerr.Conflict, "seat is occupied")
|
||
}
|
||
if current.MicSeats[index].Locked == cmd.Locked {
|
||
// 重复锁定或重复解锁是 no-op,只写 command log 做幂等保护,不广播事件。
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
|
||
// 锁状态属于 Room Cell 核心态,必须和版本递增一起提交。
|
||
current.MicSeats[index].Locked = cmd.Locked
|
||
current.Version++
|
||
|
||
// 锁麦事件进入 outbox,客户端通过系统消息修正麦位 UI。
|
||
lockEvent, err := outbox.Build(current.RoomID, "RoomMicSeatLocked", current.Version, now, &roomeventsv1.RoomMicSeatLocked{
|
||
ActorUserId: cmd.ActorUserID(),
|
||
SeatNo: cmd.SeatNo,
|
||
Locked: cmd.Locked,
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
return mutationResult{
|
||
snapshot: current.ToProto(),
|
||
syncEvent: &tencentim.RoomEvent{
|
||
EventID: lockEvent.EventID,
|
||
RoomID: current.RoomID,
|
||
EventType: "room_mic_seat_locked",
|
||
ActorUserID: cmd.ActorUserID(),
|
||
SeatNo: cmd.SeatNo,
|
||
RoomVersion: current.Version,
|
||
Attributes: map[string]string{
|
||
"locked": boolString(cmd.Locked),
|
||
},
|
||
},
|
||
}, []outbox.Record{lockEvent}, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &roomv1.SetMicSeatLockResponse{
|
||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||
Room: result.snapshot,
|
||
}, nil
|
||
}
|
||
|
||
// SetChatEnabled 开启或关闭公屏发言守卫。
|
||
func (s *Service) SetChatEnabled(ctx context.Context, req *roomv1.SetChatEnabledRequest) (*roomv1.SetChatEnabledResponse, error) {
|
||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||
// 公屏开关只影响普通客户端发言守卫,不阻断 room-service 发送系统消息。
|
||
cmd := command.SetChatEnabled{
|
||
Base: baseFromMeta(req.GetMeta()),
|
||
Enabled: req.GetEnabled(),
|
||
}
|
||
|
||
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||
if err := requireActiveRoom(current); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if err := requireManagerPresent(current, cmd.ActorUserID()); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if current.ChatEnabled == cmd.Enabled {
|
||
// 重复设置同一个开关值不产生事件,避免客户端收到无意义的状态变更。
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
|
||
// CheckSpeakPermission 后续会以该字段作为 chat_disabled 的权威来源。
|
||
current.ChatEnabled = cmd.Enabled
|
||
current.Version++
|
||
|
||
// 公屏开关变化需要进入 outbox,便于 IM 系统消息和审计消费。
|
||
chatEvent, err := outbox.Build(current.RoomID, "RoomChatEnabledChanged", current.Version, now, &roomeventsv1.RoomChatEnabledChanged{
|
||
ActorUserId: cmd.ActorUserID(),
|
||
Enabled: cmd.Enabled,
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
return mutationResult{
|
||
snapshot: current.ToProto(),
|
||
syncEvent: &tencentim.RoomEvent{
|
||
EventID: chatEvent.EventID,
|
||
RoomID: current.RoomID,
|
||
EventType: "room_chat_enabled_changed",
|
||
ActorUserID: cmd.ActorUserID(),
|
||
RoomVersion: current.Version,
|
||
Attributes: map[string]string{
|
||
"enabled": boolString(cmd.Enabled),
|
||
},
|
||
},
|
||
}, []outbox.Record{chatEvent}, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &roomv1.SetChatEnabledResponse{
|
||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||
Room: result.snapshot,
|
||
}, nil
|
||
}
|
||
|
||
// SetRoomAdmin 添加或移除房间管理员;只有 owner 可以执行。
|
||
func (s *Service) SetRoomAdmin(ctx context.Context, req *roomv1.SetRoomAdminRequest) (*roomv1.SetRoomAdminResponse, error) {
|
||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||
// 管理员身份是 RoomState 持久态,离房后保留,重新进房后继续生效。
|
||
cmd := command.SetRoomAdmin{
|
||
Base: baseFromMeta(req.GetMeta()),
|
||
TargetUserID: req.GetTargetUserId(),
|
||
Enabled: req.GetEnabled(),
|
||
}
|
||
|
||
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||
if err := requireActiveRoom(current); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if cmd.TargetUserID <= 0 {
|
||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
|
||
}
|
||
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
|
||
// 只有 owner 可以增删 admin,host/admin 不能扩张自己的权限。
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if _, exists := current.OnlineUsers[cmd.TargetUserID]; !exists {
|
||
// 首版不支持预设管理员,避免把不存在或已离房用户加入管理集合。
|
||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not in room")
|
||
}
|
||
if !cmd.Enabled && (cmd.TargetUserID == current.OwnerUserID || cmd.TargetUserID == current.HostUserID) {
|
||
// owner/host 的管理身份由专门字段决定,不能通过 admin 集合移除。
|
||
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "permission denied")
|
||
}
|
||
if current.AdminUsers[cmd.TargetUserID] == cmd.Enabled {
|
||
// 重复添加或重复移除是 no-op,避免重复发送 RoomAdminChanged。
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
|
||
if cmd.Enabled {
|
||
// map set 是管理员集合的恢复来源,快照会导出为 admin_user_ids。
|
||
current.AdminUsers[cmd.TargetUserID] = true
|
||
} else {
|
||
delete(current.AdminUsers, cmd.TargetUserID)
|
||
}
|
||
current.Version++
|
||
|
||
// 管理员变更事件同时服务客户端提示和后续房间审计。
|
||
adminEvent, err := outbox.Build(current.RoomID, "RoomAdminChanged", current.Version, now, &roomeventsv1.RoomAdminChanged{
|
||
ActorUserId: cmd.ActorUserID(),
|
||
TargetUserId: cmd.TargetUserID,
|
||
Enabled: cmd.Enabled,
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
return mutationResult{
|
||
snapshot: current.ToProto(),
|
||
syncEvent: &tencentim.RoomEvent{
|
||
EventID: adminEvent.EventID,
|
||
RoomID: current.RoomID,
|
||
EventType: "room_admin_changed",
|
||
ActorUserID: cmd.ActorUserID(),
|
||
TargetUserID: cmd.TargetUserID,
|
||
RoomVersion: current.Version,
|
||
Attributes: map[string]string{
|
||
"enabled": boolString(cmd.Enabled),
|
||
},
|
||
},
|
||
}, []outbox.Record{adminEvent}, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &roomv1.SetRoomAdminResponse{
|
||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||
Room: result.snapshot,
|
||
}, nil
|
||
}
|
||
|
||
// TransferRoomHost 转移主持人;新 host 自动进入管理员集合。
|
||
func (s *Service) TransferRoomHost(ctx context.Context, req *roomv1.TransferRoomHostRequest) (*roomv1.TransferRoomHostResponse, error) {
|
||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||
// 主持人是独立于 admin 集合的强角色,转移必须由 owner 发起。
|
||
cmd := command.TransferRoomHost{
|
||
Base: baseFromMeta(req.GetMeta()),
|
||
TargetUserID: req.GetTargetUserId(),
|
||
}
|
||
|
||
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
||
if err := requireActiveRoom(current); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if cmd.TargetUserID <= 0 {
|
||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
|
||
}
|
||
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
|
||
// host 不能自转移或把主持权限授予他人。
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if _, exists := current.OnlineUsers[cmd.TargetUserID]; !exists {
|
||
// 新 host 必须当前在房间 presence 内,保证交接对象可立即管理现场。
|
||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not in room")
|
||
}
|
||
if current.HostUserID == cmd.TargetUserID {
|
||
// 转给当前 host 不改变状态,也不重复广播。
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
|
||
oldHostUserID := current.HostUserID
|
||
// 新 host 自动进入 AdminUsers;旧 host 是否保留 admin 由当前实现保持不变。
|
||
current.HostUserID = cmd.TargetUserID
|
||
current.AdminUsers[cmd.TargetUserID] = true
|
||
current.Version++
|
||
|
||
// 主持人转移是房间管理强事件,必须进入 outbox 便于客户端和审计同步。
|
||
hostEvent, err := outbox.Build(current.RoomID, "RoomHostTransferred", current.Version, now, &roomeventsv1.RoomHostTransferred{
|
||
ActorUserId: cmd.ActorUserID(),
|
||
OldHostUserId: oldHostUserID,
|
||
NewHostUserId: cmd.TargetUserID,
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
return mutationResult{
|
||
snapshot: current.ToProto(),
|
||
syncEvent: &tencentim.RoomEvent{
|
||
EventID: hostEvent.EventID,
|
||
RoomID: current.RoomID,
|
||
EventType: "room_host_transferred",
|
||
ActorUserID: cmd.ActorUserID(),
|
||
TargetUserID: cmd.TargetUserID,
|
||
RoomVersion: current.Version,
|
||
Attributes: map[string]string{
|
||
"old_host_user_id": int64String(oldHostUserID),
|
||
"new_host_user_id": int64String(cmd.TargetUserID),
|
||
},
|
||
},
|
||
}, []outbox.Record{hostEvent}, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &roomv1.TransferRoomHostResponse{
|
||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||
Room: result.snapshot,
|
||
}, nil
|
||
}
|