package service import ( "context" "strings" roomv1 "hyapp.local/api/proto/room/v1" "hyapp/pkg/roomid" "hyapp/pkg/xerr" ) // FollowRoom 建立当前用户对房间的关注关系。 // 关注关系是用户个性化读模型事实,不改变 Room Cell 核心状态,也不需要 Redis lease 串行化。 func (s *Service) FollowRoom(ctx context.Context, req *roomv1.FollowRoomRequest) (*roomv1.FollowRoomResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) roomID, userID, err := normalizeRoomFollowInput(req.GetMeta(), req.GetRoomId(), req.GetUserId()) if err != nil { return nil, err } meta, exists, err := s.repository.GetRoomMeta(ctx, roomID) if err != nil { return nil, err } if !exists { return nil, xerr.New(xerr.NotFound, "room not found") } if isClosedRoomStatus(meta.Status) { // 已关闭房间不再建立新的关注关系;历史关注仍可通过 UnfollowRoom 幂等取消。 return nil, xerr.New(xerr.RoomClosed, "room closed") } nowMS := s.clock.Now().UnixMilli() record, err := s.repository.FollowRoom(ctx, userID, roomID, nowMS) if err != nil { return nil, err } return &roomv1.FollowRoomResponse{ RoomId: roomID, Followed: true, FollowedAtMs: record.FollowedAtMS, ServerTimeMs: nowMS, }, nil } // UnfollowRoom 取消当前用户对房间的关注关系。 // 未关注或历史房间已关闭都按幂等成功处理,方便客户端在本地状态不确定时直接收敛。 func (s *Service) UnfollowRoom(ctx context.Context, req *roomv1.UnfollowRoomRequest) (*roomv1.UnfollowRoomResponse, error) { ctx = contextFromMeta(ctx, req.GetMeta()) roomID, userID, err := normalizeRoomFollowInput(req.GetMeta(), req.GetRoomId(), req.GetUserId()) if err != nil { return nil, err } nowMS := s.clock.Now().UnixMilli() if err := s.repository.UnfollowRoom(ctx, userID, roomID, nowMS); err != nil { return nil, err } return &roomv1.UnfollowRoomResponse{ RoomId: roomID, Followed: false, ServerTimeMs: nowMS, }, nil } func normalizeRoomFollowInput(meta *roomv1.RequestMeta, rawRoomID string, userID int64) (string, int64, error) { roomID := strings.TrimSpace(rawRoomID) metaRoomID := strings.TrimSpace(meta.GetRoomId()) if roomID == "" { roomID = metaRoomID } if metaRoomID != "" && roomID != metaRoomID { // meta.room_id 是链路和审计字段;与 body/path room_id 不一致时必须拒绝,避免写错关系。 return "", 0, xerr.New(xerr.InvalidArgument, "room_id is invalid") } if !roomid.ValidStringID(roomID) { return "", 0, xerr.New(xerr.InvalidArgument, "room_id is invalid") } if userID <= 0 { return "", 0, xerr.New(xerr.InvalidArgument, "user_id is required") } return roomID, userID, nil }