222 lines
8.0 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 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
}