package service import ( "context" "strings" "time" "hyapp/pkg/appcode" "hyapp/pkg/roomid" "hyapp/pkg/xerr" roomv1 "hyapp.local/api/proto/room/v1" ) const ( roomContributionRankPeriodDay = "day" roomContributionRankPeriodWeek = "week" roomContributionRankPeriodMonth = "month" defaultRoomContributionRankLimit = 20 maxRoomContributionRankLimit = 100 ) type RoomContributionRankQuery struct { AppCode string RoomID string Period string PeriodStartMS int64 Limit int } type RoomContributionRankPage struct { Items []RoomContributionRankEntry Total int64 Period string StartAtMS int64 EndAtMS int64 ServerTimeMS int64 } type RoomContributionRankEntry struct { Rank int64 UserID int64 Score int64 GiftValue int64 UpdatedAtMS int64 } // ListRoomContributionRank 读取单房间用户贡献周期榜。 // 查询只访问 MySQL 周期读模型,不扫描 Room Cell、command log 或 outbox。 func (s *Service) ListRoomContributionRank(ctx context.Context, req *roomv1.ListRoomContributionRankRequest) (*roomv1.ListRoomContributionRankResponse, error) { ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) roomID := strings.TrimSpace(req.GetRoomId()) viewerUserID := req.GetViewerUserId() if !roomid.ValidStringID(roomID) { return nil, xerr.New(xerr.InvalidArgument, "room_id is invalid") } if viewerUserID <= 0 { return nil, xerr.New(xerr.InvalidArgument, "viewer_user_id is required") } period := normalizeRoomContributionRankPeriod(req.GetPeriod()) if period == "" { return nil, xerr.New(xerr.InvalidArgument, "period is invalid") } // 周期榜是房间内面板数据,必须确认 viewer 仍在该房间,避免用公开接口枚举任意房间用户贡献。 presence, exists, err := s.repository.GetCurrentRoomPresence(ctx, viewerUserID) if err != nil { return nil, err } if !exists || presence.RoomID != roomID || presence.Status != roomPresenceStatusActive { return nil, xerr.New(xerr.PermissionDenied, "viewer is not in room") } limit := int(req.GetLimit()) if limit <= 0 { limit = defaultRoomContributionRankLimit } if limit > maxRoomContributionRankLimit { limit = maxRoomContributionRankLimit } now := s.clock.Now().UTC() start, end := roomContributionRankWindow(period, now) result, err := s.repository.ListRoomContributionRank(ctx, RoomContributionRankQuery{ AppCode: appcode.FromContext(ctx), RoomID: roomID, Period: period, PeriodStartMS: start.UnixMilli(), Limit: limit, }) if err != nil { return nil, err } items := make([]*roomv1.RoomContributionRankItem, 0, len(result.Items)) for _, item := range result.Items { items = append(items, &roomv1.RoomContributionRankItem{ Rank: item.Rank, UserId: item.UserID, Score: item.Score, GiftValue: item.GiftValue, UpdatedAtMs: item.UpdatedAtMS, }) } return &roomv1.ListRoomContributionRankResponse{ RoomId: roomID, Period: period, StartAtMs: start.UnixMilli(), EndAtMs: end.UnixMilli(), Items: items, Total: result.Total, ServerTimeMs: now.UnixMilli(), }, nil } func roomContributionPeriodStatIncrements(app string, roomID string, userID int64, giftValue int64, occurredMS int64) []RoomUserContributionPeriodStatIncrement { if userID <= 0 || giftValue <= 0 || strings.TrimSpace(roomID) == "" { return nil } occurredAt := time.UnixMilli(occurredMS).UTC() if occurredMS <= 0 { occurredAt = time.Now().UTC() occurredMS = occurredAt.UnixMilli() } stats := make([]RoomUserContributionPeriodStatIncrement, 0, 3) for _, period := range []string{roomContributionRankPeriodDay, roomContributionRankPeriodWeek, roomContributionRankPeriodMonth} { start, end := roomContributionRankWindow(period, occurredAt) stats = append(stats, RoomUserContributionPeriodStatIncrement{ AppCode: app, RoomID: roomID, Period: period, PeriodStartMS: start.UnixMilli(), PeriodEndMS: end.UnixMilli(), UserID: userID, GiftValue: giftValue, LastGiftAtMS: occurredMS, }) } return stats } func normalizeRoomContributionRankPeriod(raw string) string { switch strings.ToLower(strings.TrimSpace(raw)) { case "", roomContributionRankPeriodDay, "today", "daily": return roomContributionRankPeriodDay case roomContributionRankPeriodWeek, "weekly": return roomContributionRankPeriodWeek case roomContributionRankPeriodMonth, "monthly": return roomContributionRankPeriodMonth default: return "" } } func roomContributionRankWindow(period string, now time.Time) (time.Time, time.Time) { dayStart := time.Date(now.UTC().Year(), now.UTC().Month(), now.UTC().Day(), 0, 0, 0, 0, time.UTC) switch period { case roomContributionRankPeriodWeek: weekday := int(dayStart.Weekday()) if weekday == 0 { weekday = 7 } start := dayStart.AddDate(0, 0, 1-weekday) return start, start.AddDate(0, 0, 7) case roomContributionRankPeriodMonth: start := time.Date(dayStart.Year(), dayStart.Month(), 1, 0, 0, 0, 0, time.UTC) return start, start.AddDate(0, 1, 0) default: return dayStart, dayStart.AddDate(0, 0, 1) } }