2026-05-12 09:53:20 +08:00

154 lines
4.7 KiB
Go

package service
import (
"context"
"log/slog"
"slices"
roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/logx"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/room/state"
)
const (
roomPresenceStatusActive = "active"
roomPresenceStatusLeft = "left"
)
// GetCurrentRoom 返回当前用户可恢复房间的只读探测结果。
// 该方法先查用户级 presence 读模型,再用 Room Cell 快照二次校验,避免读模型滞后导致恢复已关闭或已踢出的房间。
func (s *Service) GetCurrentRoom(ctx context.Context, req *roomv1.GetCurrentRoomRequest) (*roomv1.GetCurrentRoomResponse, error) {
ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode())
userID := req.GetUserId()
if userID <= 0 {
return nil, xerr.New(xerr.InvalidArgument, "user_id is required")
}
serverTimeMS := s.clock.Now().UnixMilli()
presence, exists, err := s.repository.GetCurrentRoomPresence(ctx, userID)
if err != nil {
return nil, err
}
if !exists {
return emptyCurrentRoomResponse(serverTimeMS), nil
}
snapshot, err := s.currentSnapshot(ctx, presence.RoomID)
if err != nil {
return nil, err
}
if !currentRoomSnapshotRestorable(snapshot, userID) {
// 查询语义必须保持只读;读模型滞后由后续命令投影或专项补偿修复。
return emptyCurrentRoomResponse(serverTimeMS), nil
}
roomUser := findProtoUser(snapshot, userID)
seat := protoSeatByUser(snapshot, userID)
role := presence.Role
if roomUser != nil {
role = roomUser.GetRole()
}
publishState := ""
micSessionID := ""
needRTCToken := false
if seat != nil {
publishState = seat.GetPublishState()
micSessionID = seat.GetMicSessionId()
needRTCToken = publishState == state.MicPublishPending || publishState == state.MicPublishPublishing
}
return &roomv1.GetCurrentRoomResponse{
HasCurrentRoom: true,
RoomId: snapshot.GetRoomId(),
RoomVersion: snapshot.GetVersion(),
Role: role,
MicSessionId: micSessionID,
PublishState: publishState,
NeedJoinImGroup: true,
NeedRtcToken: needRTCToken,
ServerTimeMs: serverTimeMS,
}, nil
}
func emptyCurrentRoomResponse(serverTimeMS int64) *roomv1.GetCurrentRoomResponse {
return &roomv1.GetCurrentRoomResponse{ServerTimeMs: serverTimeMS}
}
func currentRoomSnapshotRestorable(snapshot *roomv1.RoomSnapshot, userID int64) bool {
if snapshot == nil || snapshot.GetRoomId() == "" || snapshot.GetStatus() != state.RoomStatusActive {
return false
}
if containsUserID(snapshot.GetBanUserIds(), userID) {
return false
}
return findProtoUser(snapshot, userID) != nil
}
// projectRoomPresenceBestEffort 用最新快照刷新“用户当前房间”读模型。
// 读模型失败不能影响命令成功语义,但必须记录日志,避免恢复入口长期缺失无人感知。
func (s *Service) projectRoomPresenceBestEffort(ctx context.Context, snapshot *roomv1.RoomSnapshot) {
projection := roomPresenceProjectionFromSnapshot(snapshot, s.clock.Now().UnixMilli())
if projection.RoomID == "" {
return
}
if err := s.repository.ProjectRoomPresence(ctx, projection); err != nil {
logx.Error(ctx, "room_presence_project_failed", err,
slog.String("component", "room_presence_projector"),
slog.String("room_id", projection.RoomID),
slog.Int64("room_version", projection.RoomVersion),
)
}
}
func roomPresenceProjectionFromSnapshot(snapshot *roomv1.RoomSnapshot, updatedAtMS int64) RoomPresenceSnapshot {
if snapshot == nil || snapshot.GetRoomId() == "" {
return RoomPresenceSnapshot{}
}
seatsByUser := make(map[int64]*roomv1.SeatState, len(snapshot.GetMicSeats()))
for _, seat := range snapshot.GetMicSeats() {
if seat.GetUserId() > 0 {
seatsByUser[seat.GetUserId()] = seat
}
}
users := make([]RoomPresence, 0, len(snapshot.GetOnlineUsers()))
if snapshot.GetStatus() == state.RoomStatusActive {
for _, user := range snapshot.GetOnlineUsers() {
if user.GetUserId() <= 0 {
continue
}
entry := RoomPresence{
AppCode: snapshot.GetAppCode(),
UserID: user.GetUserId(),
RoomID: snapshot.GetRoomId(),
Role: user.GetRole(),
RoomVersion: snapshot.GetVersion(),
Status: roomPresenceStatusActive,
LastSeenAtMS: user.GetLastSeenAtMs(),
UpdatedAtMS: updatedAtMS,
}
if seat := seatsByUser[user.GetUserId()]; seat != nil {
entry.PublishState = seat.GetPublishState()
entry.MicSessionID = seat.GetMicSessionId()
}
users = append(users, entry)
}
}
return RoomPresenceSnapshot{
AppCode: snapshot.GetAppCode(),
RoomID: snapshot.GetRoomId(),
RoomVersion: snapshot.GetVersion(),
RoomStatus: snapshot.GetStatus(),
Users: users,
UpdatedAtMS: updatedAtMS,
}
}
func containsUserID(values []int64, target int64) bool {
return slices.Contains(values, target)
}