220 lines
7.3 KiB
Go
220 lines
7.3 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
roomeventsv1 "hyapp/api/proto/events/room/v1"
|
|
roomv1 "hyapp/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"
|
|
)
|
|
|
|
// MicUp 把目标用户放到指定麦位。
|
|
func (s *Service) MicUp(ctx context.Context, req *roomv1.MicUpRequest) (*roomv1.MicUpResponse, error) {
|
|
// MicUp 以 actor 作为上麦目标,管理员代操作走 MicDown/ChangeMicSeat 等显式 target 命令。
|
|
cmd := command.MicUp{
|
|
Base: baseFromMeta(req.GetMeta()),
|
|
SeatNo: req.GetSeatNo(),
|
|
}
|
|
|
|
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
|
if _, exists := current.OnlineUsers[cmd.ActorUserID()]; !exists {
|
|
// 不在 room-service presence 的用户不能上麦。
|
|
return mutationResult{}, nil, xerr.New(xerr.NotFound, "user not in room")
|
|
}
|
|
|
|
if _, exists := current.SeatByUser(cmd.ActorUserID()); exists {
|
|
// 一个用户同一时间只能占一个麦位。
|
|
return mutationResult{}, nil, xerr.New(xerr.Conflict, "user already on seat")
|
|
}
|
|
|
|
index := current.SeatIndex(cmd.SeatNo)
|
|
if index < 0 {
|
|
// 麦位编号必须来自房间初始化的 MicSeats。
|
|
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "seat does not exist")
|
|
}
|
|
|
|
if current.MicSeats[index].Locked {
|
|
// 锁定麦位不能被普通上麦命令占用。
|
|
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "seat is locked")
|
|
}
|
|
|
|
if current.MicSeats[index].UserID != 0 {
|
|
// 非空麦位不能被覆盖,避免两个用户同时占同一麦位。
|
|
return mutationResult{}, nil, xerr.New(xerr.Conflict, "seat is occupied")
|
|
}
|
|
|
|
// 麦位占用和房间版本必须在同一个 Room Cell 任务里更新。
|
|
current.MicSeats[index].UserID = cmd.ActorUserID()
|
|
current.Version++
|
|
|
|
// RoomMicChanged 是房间外消费者和腾讯云 IM 共同使用的麦位事件源。
|
|
micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, &roomeventsv1.RoomMicChanged{
|
|
ActorUserId: cmd.ActorUserID(),
|
|
TargetUserId: cmd.ActorUserID(),
|
|
FromSeat: 0,
|
|
ToSeat: cmd.SeatNo,
|
|
Action: "up",
|
|
})
|
|
if err != nil {
|
|
return mutationResult{}, nil, err
|
|
}
|
|
|
|
return mutationResult{
|
|
snapshot: current.ToProto(),
|
|
seatNo: cmd.SeatNo,
|
|
syncEvent: &tencentim.RoomEvent{
|
|
EventID: micEvent.EventID,
|
|
RoomID: current.RoomID,
|
|
EventType: "room_mic_up",
|
|
ActorUserID: cmd.ActorUserID(),
|
|
TargetUserID: cmd.ActorUserID(),
|
|
SeatNo: cmd.SeatNo,
|
|
RoomVersion: current.Version,
|
|
},
|
|
}, []outbox.Record{micEvent}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &roomv1.MicUpResponse{
|
|
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
|
SeatNo: result.seatNo,
|
|
Room: result.snapshot,
|
|
}, nil
|
|
}
|
|
|
|
// MicDown 把目标用户从当前麦位移下。
|
|
func (s *Service) MicDown(ctx context.Context, req *roomv1.MicDownRequest) (*roomv1.MicDownResponse, error) {
|
|
// MicDown 支持操作者和目标用户不同,权限策略后续可在命令入口扩展。
|
|
cmd := command.MicDown{
|
|
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) {
|
|
seat, exists := current.SeatByUser(cmd.TargetUserID)
|
|
if !exists {
|
|
// 目标用户不在麦上时不能生成下麦事件。
|
|
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not on seat")
|
|
}
|
|
|
|
// SeatByUser 返回的是值拷贝,真正修改需要再定位切片下标。
|
|
index := current.SeatIndex(seat.SeatNo)
|
|
current.MicSeats[index].UserID = 0
|
|
current.Version++
|
|
|
|
micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, &roomeventsv1.RoomMicChanged{
|
|
ActorUserId: cmd.ActorUserID(),
|
|
TargetUserId: cmd.TargetUserID,
|
|
FromSeat: seat.SeatNo,
|
|
ToSeat: 0,
|
|
Action: "down",
|
|
})
|
|
if err != nil {
|
|
return mutationResult{}, nil, err
|
|
}
|
|
|
|
return mutationResult{
|
|
snapshot: current.ToProto(),
|
|
seatNo: seat.SeatNo,
|
|
syncEvent: &tencentim.RoomEvent{
|
|
EventID: micEvent.EventID,
|
|
RoomID: current.RoomID,
|
|
EventType: "room_mic_down",
|
|
ActorUserID: cmd.ActorUserID(),
|
|
TargetUserID: cmd.TargetUserID,
|
|
SeatNo: seat.SeatNo,
|
|
RoomVersion: current.Version,
|
|
},
|
|
}, []outbox.Record{micEvent}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &roomv1.MicDownResponse{
|
|
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
|
SeatNo: result.seatNo,
|
|
Room: result.snapshot,
|
|
}, nil
|
|
}
|
|
|
|
// ChangeMicSeat 调整指定用户所在麦位。
|
|
func (s *Service) ChangeMicSeat(ctx context.Context, req *roomv1.ChangeMicSeatRequest) (*roomv1.ChangeMicSeatResponse, error) {
|
|
// ChangeMicSeat 是原子换位:从原麦位清空和写入新麦位必须同版本完成。
|
|
cmd := command.ChangeMicSeat{
|
|
Base: baseFromMeta(req.GetMeta()),
|
|
TargetUserID: req.GetTargetUserId(),
|
|
SeatNo: req.GetSeatNo(),
|
|
}
|
|
|
|
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
|
|
fromSeat, exists := current.SeatByUser(cmd.TargetUserID)
|
|
if !exists {
|
|
// 未上麦用户没有可换出的源麦位。
|
|
return mutationResult{}, nil, xerr.New(xerr.NotFound, "target not on seat")
|
|
}
|
|
|
|
toIndex := current.SeatIndex(cmd.SeatNo)
|
|
if toIndex < 0 {
|
|
// 目标麦位必须存在。
|
|
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "seat does not exist")
|
|
}
|
|
|
|
if current.MicSeats[toIndex].Locked {
|
|
// 锁定麦位不能换入。
|
|
return mutationResult{}, nil, xerr.New(xerr.PermissionDenied, "seat is locked")
|
|
}
|
|
|
|
if current.MicSeats[toIndex].UserID != 0 {
|
|
// 目标麦位被占用时拒绝换位,避免覆盖另一个用户。
|
|
return mutationResult{}, nil, xerr.New(xerr.Conflict, "seat is occupied")
|
|
}
|
|
|
|
// 清空源麦位和占用目标麦位在同一 Cell 任务内完成,客户端不会看到中间态。
|
|
fromIndex := current.SeatIndex(fromSeat.SeatNo)
|
|
current.MicSeats[fromIndex].UserID = 0
|
|
current.MicSeats[toIndex].UserID = cmd.TargetUserID
|
|
current.Version++
|
|
|
|
micEvent, err := outbox.Build(current.RoomID, "RoomMicChanged", current.Version, now, &roomeventsv1.RoomMicChanged{
|
|
ActorUserId: cmd.ActorUserID(),
|
|
TargetUserId: cmd.TargetUserID,
|
|
FromSeat: fromSeat.SeatNo,
|
|
ToSeat: cmd.SeatNo,
|
|
Action: "change",
|
|
})
|
|
if err != nil {
|
|
return mutationResult{}, nil, err
|
|
}
|
|
|
|
return mutationResult{
|
|
snapshot: current.ToProto(),
|
|
syncEvent: &tencentim.RoomEvent{
|
|
EventID: micEvent.EventID,
|
|
RoomID: current.RoomID,
|
|
EventType: "room_mic_change",
|
|
ActorUserID: cmd.ActorUserID(),
|
|
TargetUserID: cmd.TargetUserID,
|
|
SeatNo: cmd.SeatNo,
|
|
RoomVersion: current.Version,
|
|
},
|
|
}, []outbox.Record{micEvent}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &roomv1.ChangeMicSeatResponse{
|
|
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
|
Room: result.snapshot,
|
|
}, nil
|
|
}
|