260 lines
9.1 KiB
Go
260 lines
9.1 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"
|
||
)
|
||
|
||
// MuteUser 修改用户禁言状态。
|
||
func (s *Service) MuteUser(ctx context.Context, req *roomv1.MuteUserRequest) (*roomv1.MuteUserResponse, error) {
|
||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||
// 禁言状态由 room-service 持有,腾讯云 IM 发言前回调必须同步查询 CheckSpeakPermission。
|
||
cmd := command.MuteUser{
|
||
Base: baseFromMeta(req.GetMeta()),
|
||
TargetUserID: req.GetTargetUserId(),
|
||
Muted: req.GetMuted(),
|
||
}
|
||
|
||
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 cmd.TargetUserID == cmd.ActorUserID() {
|
||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "cannot mute self")
|
||
}
|
||
if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if err := canManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if current.MuteUsers[cmd.TargetUserID] == cmd.Muted {
|
||
// 重复设置同一禁言状态是业务 no-op,仍由 pipeline 写 command log 做幂等保护。
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
|
||
if cmd.Muted {
|
||
// map set 表达禁言集合,true 才会被快照导出。
|
||
current.MuteUsers[cmd.TargetUserID] = true
|
||
} else {
|
||
// 解除禁言直接删除集合成员。
|
||
delete(current.MuteUsers, cmd.TargetUserID)
|
||
}
|
||
|
||
current.Version++
|
||
|
||
// 禁言变更进入 outbox,外部系统可以做审核和操作审计。
|
||
muteEvent, err := outbox.Build(current.RoomID, "RoomUserMuted", current.Version, now, &roomeventsv1.RoomUserMuted{
|
||
ActorUserId: cmd.ActorUserID(),
|
||
TargetUserId: cmd.TargetUserID,
|
||
Muted: cmd.Muted,
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
return mutationResult{
|
||
snapshot: current.ToProto(),
|
||
syncEvent: &tencentim.RoomEvent{
|
||
EventID: muteEvent.EventID,
|
||
RoomID: current.RoomID,
|
||
EventType: "room_user_muted",
|
||
ActorUserID: cmd.ActorUserID(),
|
||
TargetUserID: cmd.TargetUserID,
|
||
RoomVersion: current.Version,
|
||
},
|
||
}, []outbox.Record{muteEvent}, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &roomv1.MuteUserResponse{
|
||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||
Room: result.snapshot,
|
||
}, nil
|
||
}
|
||
|
||
// KickUser 把目标用户踢出房间并移除其麦位与 presence。
|
||
func (s *Service) KickUser(ctx context.Context, req *roomv1.KickUserRequest) (*roomv1.KickUserResponse, error) {
|
||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||
// KickUser 同时修改 presence、麦位和 ban 集合,是典型 Room Cell 强关联命令。
|
||
cmd := command.KickUser{
|
||
Base: baseFromMeta(req.GetMeta()),
|
||
TargetUserID: req.GetTargetUserId(),
|
||
DurationMS: req.GetDurationMs(),
|
||
}
|
||
|
||
shouldKickRTC := false
|
||
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 cmd.TargetUserID == cmd.ActorUserID() {
|
||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "cannot kick self")
|
||
}
|
||
if cmd.DurationMS < 0 {
|
||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "duration_ms must be non-negative")
|
||
}
|
||
if err := requireActorPresent(current, cmd.ActorUserID()); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if err := canManageTarget(current, cmd.ActorUserID(), cmd.TargetUserID); err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
_, inRoom := current.OnlineUsers[cmd.TargetUserID]
|
||
_, onSeat := current.SeatByUser(cmd.TargetUserID)
|
||
// 只有目标确实还在业务房间或麦位上,才需要请求 TRTC 服务端移除实时音频连接。
|
||
shouldKickRTC = inRoom || onSeat
|
||
|
||
var releasedSeatNo int32
|
||
var releasedMicSessionID string
|
||
if seat, exists := current.SeatByUser(cmd.TargetUserID); exists {
|
||
// 被踢用户如果在麦上,必须先释放麦位,避免恢复后出现幽灵占位。
|
||
index := current.SeatIndex(seat.SeatNo)
|
||
releasedSeatNo = seat.SeatNo
|
||
releasedMicSessionID = current.MicSeats[index].MicSessionID
|
||
current.ClearMicSession(index)
|
||
}
|
||
|
||
// ban 集合会让后续 JoinRoom 和 VerifyRoomPresence 都失败。
|
||
delete(current.OnlineUsers, cmd.TargetUserID)
|
||
delete(current.AdminUsers, cmd.TargetUserID)
|
||
expiresAtMS := int64(0)
|
||
nowMS := now.UnixMilli()
|
||
if cmd.DurationMS > 0 {
|
||
expiresAtMS = nowMS + cmd.DurationMS
|
||
if expiresAtMS <= nowMS {
|
||
return mutationResult{}, nil, xerr.New(xerr.InvalidArgument, "duration_ms is too large")
|
||
}
|
||
}
|
||
current.BanUsers[cmd.TargetUserID] = state.UserModerationState{
|
||
UserID: cmd.TargetUserID,
|
||
ActorUserID: cmd.ActorUserID(),
|
||
CreatedAtMS: nowMS,
|
||
ExpiresAtMS: expiresAtMS,
|
||
}
|
||
current.Version++
|
||
|
||
kickEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version, now, &roomeventsv1.RoomUserKicked{
|
||
ActorUserId: cmd.ActorUserID(),
|
||
TargetUserId: cmd.TargetUserID,
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
records := make([]outbox.Record, 0, 2)
|
||
if releasedMicSessionID != "" {
|
||
// 踢人释放麦位必须也生成 down 事实,时长聚合不能从 RoomUserKicked 反推麦位结束。
|
||
micEvent, err := buildMicDownEventForSeat(current.RoomID, current.Version, now, cmd.ActorUserID(), cmd.TargetUserID, releasedSeatNo, releasedMicSessionID, "kick")
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
records = append(records, micEvent)
|
||
}
|
||
records = append(records, kickEvent)
|
||
|
||
return mutationResult{
|
||
snapshot: current.ToProto(),
|
||
syncEvent: &tencentim.RoomEvent{
|
||
EventID: kickEvent.EventID,
|
||
RoomID: current.RoomID,
|
||
EventType: "room_user_kicked",
|
||
ActorUserID: cmd.ActorUserID(),
|
||
TargetUserID: cmd.TargetUserID,
|
||
RoomVersion: current.Version,
|
||
},
|
||
}, records, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
resp := &roomv1.KickUserResponse{
|
||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||
Room: result.snapshot,
|
||
}
|
||
if result.applied && shouldKickRTC && s.rtcUserRemover != nil {
|
||
// RTC 是房间外连接态:踢人事实已提交后再调用 TRTC,失败不回滚 Room Cell,但要回传给调用方做告警。
|
||
if err := s.rtcUserRemover.RemoveUserByStrRoomID(ctx, cmd.RoomID(), cmd.TargetUserID); err != nil {
|
||
resp.RtcKickError = err.Error()
|
||
} else {
|
||
resp.RtcKicked = true
|
||
}
|
||
}
|
||
|
||
return resp, nil
|
||
}
|
||
|
||
// UnbanUser 解除房间 ban;不会恢复 presence、管理员、麦位或 RTC 推流事实。
|
||
func (s *Service) UnbanUser(ctx context.Context, req *roomv1.UnbanUserRequest) (*roomv1.UnbanUserResponse, error) {
|
||
ctx = contextFromMeta(ctx, req.GetMeta())
|
||
cmd := command.UnbanUser{
|
||
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 {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
if _, exists := current.BanUsers[cmd.TargetUserID]; !exists {
|
||
// 重复 unban 是 no-op,仍写 command log 确保 command_id 幂等可判定。
|
||
return mutationResult{snapshot: current.ToProto()}, nil, nil
|
||
}
|
||
|
||
delete(current.BanUsers, cmd.TargetUserID)
|
||
current.Version++
|
||
|
||
unbanEvent, err := outbox.Build(current.RoomID, "RoomUserUnbanned", current.Version, now, &roomeventsv1.RoomUserUnbanned{
|
||
ActorUserId: cmd.ActorUserID(),
|
||
TargetUserId: cmd.TargetUserID,
|
||
})
|
||
if err != nil {
|
||
return mutationResult{}, nil, err
|
||
}
|
||
|
||
return mutationResult{
|
||
snapshot: current.ToProto(),
|
||
syncEvent: &tencentim.RoomEvent{
|
||
EventID: unbanEvent.EventID,
|
||
RoomID: current.RoomID,
|
||
EventType: "room_user_unbanned",
|
||
ActorUserID: cmd.ActorUserID(),
|
||
TargetUserID: cmd.TargetUserID,
|
||
RoomVersion: current.Version,
|
||
},
|
||
}, []outbox.Record{unbanEvent}, nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &roomv1.UnbanUserResponse{
|
||
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
|
||
Room: result.snapshot,
|
||
}, nil
|
||
}
|