hyapp-server/services/room-service/internal/room/service/contribution_rank_query.go
2026-07-06 12:43:40 +08:00

172 lines
5.1 KiB
Go

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)
}
}