package service import ( "context" "google.golang.org/protobuf/proto" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/appcode" "hyapp/services/room-service/internal/room/cell" "hyapp/services/room-service/internal/room/rank" ) // currentSnapshot 返回当前房间的最新快照;没有内存 Cell 时会从持久化恢复后再返回。 func (s *Service) currentSnapshot(ctx context.Context, roomID string) (*roomv1.RoomSnapshot, error) { if roomCell := s.loadCell(ctx, roomID); roomCell != nil { // 内存 Cell 是最新状态来源,guard 查询优先读它。 currentState, _, err := roomCell.Snapshot(ctx) if err != nil { return nil, err } return snapshotWithApp(ctx, currentState.ToProto()), nil } // 没有内存 Cell 时从 meta + snapshot + command log 恢复,避免守卫读到落后于 command log 的旧快照。 roomMeta, exists, err := s.repository.GetRoomMeta(ctx, roomID) if err != nil || !exists { return nil, err } restoredState, err := s.recoverRoom(ctx, roomMeta) if err != nil { // 恢复失败说明持久化状态不可用,不能让外部 IM 入口自行放行。 return nil, err } if s.isDraining() { // 下线期间守卫查询可以读取恢复后的快照,但不能再创建新的本地 Room Cell。 return snapshotWithApp(ctx, restoredState.ToProto()), nil } roomCell := cell.New(restoredState, rank.FromState(restoredState.GiftRank, s.rankLimit)) s.installCell(ctx, roomID, roomCell) return snapshotWithApp(ctx, restoredState.ToProto()), nil } // persistSnapshot 持久化 protobuf 快照,作为后续恢复和 guard 查询的快速入口。 func (s *Service) persistSnapshot(ctx context.Context, snapshot *roomv1.RoomSnapshot) error { snapshot = snapshotWithApp(ctx, snapshot) // 快照统一使用 protobuf,保持和跨服务契约一致。 payload, err := proto.Marshal(snapshot) if err != nil { return err } return s.repository.SaveSnapshot(ctx, SnapshotRecord{ AppCode: snapshot.GetAppCode(), RoomID: snapshot.GetRoomId(), RoomVersion: snapshot.GetVersion(), Payload: payload, CreatedAtMS: s.clock.Now().UnixMilli(), }) } func snapshotWithApp(ctx context.Context, snapshot *roomv1.RoomSnapshot) *roomv1.RoomSnapshot { if snapshot == nil { return nil } // RoomState 本身不保存租户上下文,快照出服务边界前统一补 app_code。 snapshot.AppCode = appcode.FromContext(ctx) if snapshot.RoomShortId == "" && snapshot.GetRoomExt() != nil { // room_short_id 已提升为顶层字段,但旧快照/恢复路径仍可能只存在于 RoomExt。 snapshot.RoomShortId = snapshot.GetRoomExt()["room_short_id"] } return snapshot } // shouldSnapshot 判定当前命令结果是否需要落快照。 func shouldSnapshot(snapshot *roomv1.RoomSnapshot, every int64) bool { if snapshot == nil || every <= 1 { // every<=1 表示每次有快照结果都保存,nil 快照永远不保存。 return snapshot != nil } // 按房间版本取模做简单采样,避免引入额外计数状态。 return snapshot.GetVersion()%every == 0 }