222 lines
8.0 KiB
Go
222 lines
8.0 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,避免管理员扩张自己的权限。
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if _, exists := current.OnlineUsers[cmd.TargetUserID]; !exists {
|
||
// 首版不支持预设管理员,避免把不存在或已离房用户加入管理集合。
|
||
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not in room")
|
||
}
|
||
if cmd.TargetUserID == current.OwnerUserID {
|
||
// owner 不是管理员,不能对 owner 执行设置或取消管理员。
|
||
return mutationResult{}, nil, xerr.New(xerr.Conflict, "owner cannot be room admin target")
|
||
}
|
||
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
|
||
}
|