diff --git a/models/leaderboard.go b/models/leaderboard.go index b09b907..b92119d 100644 --- a/models/leaderboard.go +++ b/models/leaderboard.go @@ -19,14 +19,36 @@ type LeaderboardItem struct { CreatedAt CustomTime `gorm:"type:timestamp; default:CURRENT_TIMESTAMP" swaggertype:"string" format:"date" example:"2006-01-02 15:04:05.000"` } +func (l1 *LeaderboardItem) Equals(l2 *LeaderboardItem) bool { + return l1.ID == l2.ID +} + type Leaderboard []*LeaderboardItem +func (l *Leaderboard) Add(item *LeaderboardItem) { + if _, found := slice.Find[*LeaderboardItem](*l, func(i int, item2 *LeaderboardItem) bool { + return item.Equals(item2) + }); !found { + *l = append(*l, item) + } +} + +func (l *Leaderboard) AddMany(items []*LeaderboardItem) { + for _, item := range items { + l.Add(item) + } +} + func (l Leaderboard) UserIDs() []string { return slice.Unique[string](slice.Map[*LeaderboardItem, string](l, func(i int, item *LeaderboardItem) string { return item.UserID })) } +func (l Leaderboard) HasUser(userId string) bool { + return slice.Contain(l.UserIDs(), userId) +} + func (l Leaderboard) TopByKey(by uint8, key string) Leaderboard { return slice.Filter[*LeaderboardItem](l, func(i int, item *LeaderboardItem) bool { return item.By != nil && *item.By == by && item.Key != nil && strings.ToLower(*item.Key) == strings.ToLower(key) diff --git a/repositories/leaderboard.go b/repositories/leaderboard.go index 4bc819c..b9963ea 100644 --- a/repositories/leaderboard.go +++ b/repositories/leaderboard.go @@ -33,6 +33,15 @@ func (r *LeaderboardRepository) CountAllByUser(userId string) (int64, error) { return count, err } +func (r *LeaderboardRepository) CountUsers() (int64, error) { + var count int64 + err := r.db. + Table("leaderboard_items"). + Distinct("user_id"). + Count(&count).Error + return count, err +} + func (r *LeaderboardRepository) GetAllAggregatedByInterval(key *models.IntervalKey, by *uint8, limit, skip int) ([]*models.LeaderboardItem, error) { // TODO: distinct by (user, key) to filter out potential duplicates ? var items []*models.LeaderboardItem @@ -56,11 +65,10 @@ func (r *LeaderboardRepository) GetAggregatedByUserAndInterval(userId string, ke subq := r.db. Table("leaderboard_items"). Select("*, rank() over (partition by \"key\" order by total desc) as \"rank\""). - Where("user_id = ?", userId). Where("\"interval\" in ?", *key) subq = utils.WhereNullable(subq, "\"by\"", by) - q := r.db.Table("(?) as ranked", subq) + q := r.db.Table("(?) as ranked", subq).Where("user_id = ?", userId) q = r.withPaging(q, limit, skip) if err := q.Find(&items).Error; err != nil { diff --git a/repositories/repositories.go b/repositories/repositories.go index 1ce9b3b..d700d83 100644 --- a/repositories/repositories.go +++ b/repositories/repositories.go @@ -90,6 +90,7 @@ type IUserRepository interface { type ILeaderboardRepository interface { InsertBatch([]*models.LeaderboardItem) error CountAllByUser(string) (int64, error) + CountUsers() (int64, error) DeleteByUser(string) error DeleteByUserAndInterval(string, *models.IntervalKey) error GetAllAggregatedByInterval(*models.IntervalKey, *uint8, int, int) ([]*models.LeaderboardItem, error) diff --git a/routes/leaderboard.go b/routes/leaderboard.go index dd1305c..8e1ad4c 100644 --- a/routes/leaderboard.go +++ b/routes/leaderboard.go @@ -57,7 +57,10 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi user := middlewares.GetPrincipal(r) byParam := strings.ToLower(r.URL.Query().Get("by")) keyParam := strings.ToLower(r.URL.Query().Get("key")) - pageParams := utils.ParsePageParams(r) + pageParams := utils.ParsePageParamsWithDefault(r, 1, 100) + // note: pagination is not fully implemented, yet + // count function to get total item / total pages is missing + // and according ui (+ optionally search bar) is missing, too var err error var leaderboard models.Leaderboard @@ -70,6 +73,16 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi conf.Log().Request(r).Error("error while fetching general leaderboard items - %v", err) return &view.LeaderboardViewModel{Error: criticalError} } + + // regardless of page, always show own rank + if user != nil && !leaderboard.HasUser(user.ID) { + // but only if leaderboard spans multiple pages + if count, err := h.leaderboardService.CountUsers(); err == nil && count > int64(pageParams.PageSize) { + if l, err := h.leaderboardService.GetByIntervalAndUser(models.IntervalPast7Days, user.ID, true); err == nil && len(l) > 0 { + leaderboard = append(leaderboard, l[0]) + } + } + } } else { if by, ok := allowedAggregations[byParam]; ok { leaderboard, err = h.leaderboardService.GetAggregatedByInterval(models.IntervalPast7Days, &by, pageParams, true) @@ -78,6 +91,18 @@ func (h *LeaderboardHandler) buildViewModel(r *http.Request) *view.LeaderboardVi return &view.LeaderboardViewModel{Error: criticalError} } + // regardless of page, always show own rank + if user != nil { + // but only if leaderboard could, in theory, span multiple pages + if count, err := h.leaderboardService.CountUsers(); err == nil && count > int64(pageParams.PageSize) { + if l, err := h.leaderboardService.GetAggregatedByIntervalAndUser(models.IntervalPast7Days, user.ID, &by, true); err == nil { + leaderboard.AddMany(l) + } else { + conf.Log().Request(r).Error("error while fetching own aggregated user leaderboard - %v", err) + } + } + } + userLeaderboards := slice.GroupWith[*models.LeaderboardItem, string](leaderboard, func(item *models.LeaderboardItem) string { return item.UserID }) diff --git a/services/leaderboard.go b/services/leaderboard.go index 07105bf..136c62d 100644 --- a/services/leaderboard.go +++ b/services/leaderboard.go @@ -126,13 +126,31 @@ func (srv *LeaderboardService) ExistsAnyByUser(userId string) (bool, error) { return count > 0, err } +func (srv *LeaderboardService) CountUsers() (int64, error) { + // check cache + cacheKey := "count_total" + if cacheResult, ok := srv.cache.Get(cacheKey); ok { + return cacheResult.(int64), nil + } + + count, err := srv.repository.CountUsers() + if err != nil { + srv.cache.SetDefault(cacheKey, count) + } + return count, err +} + func (srv *LeaderboardService) GetByInterval(interval *models.IntervalKey, pageParams *models.PageParams, resolveUsers bool) (models.Leaderboard, error) { return srv.GetAggregatedByInterval(interval, nil, pageParams, resolveUsers) } +func (srv *LeaderboardService) GetByIntervalAndUser(interval *models.IntervalKey, userId string, resolveUser bool) (models.Leaderboard, error) { + return srv.GetAggregatedByIntervalAndUser(interval, userId, nil, resolveUser) +} + func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.IntervalKey, by *uint8, pageParams *models.PageParams, resolveUsers bool) (models.Leaderboard, error) { // check cache - cacheKey := srv.getHash(interval, by, pageParams) + cacheKey := srv.getHash(interval, by, "", pageParams) if cacheResult, ok := srv.cache.Get(cacheKey); ok { return cacheResult.([]*models.LeaderboardItem), nil } @@ -143,8 +161,6 @@ func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.Interval } if resolveUsers { - a := models.Leaderboard(items).UserIDs() - println(a) users, err := srv.userService.GetManyMapped(models.Leaderboard(items).UserIDs()) if err != nil { config.Log().Error("failed to resolve users for leaderboard item - %v", err) @@ -161,6 +177,33 @@ func (srv *LeaderboardService) GetAggregatedByInterval(interval *models.Interval return items, nil } +func (srv *LeaderboardService) GetAggregatedByIntervalAndUser(interval *models.IntervalKey, userId string, by *uint8, resolveUser bool) (models.Leaderboard, error) { + // check cache + cacheKey := srv.getHash(interval, by, userId, nil) + if cacheResult, ok := srv.cache.Get(cacheKey); ok { + return cacheResult.([]*models.LeaderboardItem), nil + } + + items, err := srv.repository.GetAggregatedByUserAndInterval(userId, interval, by, 0, 0) + if err != nil { + return nil, err + } + + if resolveUser { + u, err := srv.userService.GetUserById(userId) + if err != nil { + config.Log().Error("failed to resolve user for leaderboard item - %v", err) + } else { + for _, item := range items { + item.User = u + } + } + } + + srv.cache.SetDefault(cacheKey, items) + return items, nil +} + func (srv *LeaderboardService) GenerateByUser(user *models.User, interval *models.IntervalKey) (*models.LeaderboardItem, error) { err, from, to := utils.ResolveIntervalTZ(interval, user.TZ()) if err != nil { @@ -209,11 +252,13 @@ func (srv *LeaderboardService) GenerateAggregatedByUser(user *models.User, inter return items, nil } -func (srv *LeaderboardService) getHash(interval *models.IntervalKey, by *uint8, pageParams *models.PageParams) string { - k := strings.Join(*interval, "__") +func (srv *LeaderboardService) getHash(interval *models.IntervalKey, by *uint8, user string, pageParams *models.PageParams) string { + k := strings.Join(*interval, "__") + "__" + user if by != nil && !reflect.ValueOf(by).IsNil() { k += "__" + models.GetEntityColumn(*by) } - k += "__" + strconv.Itoa(pageParams.Page) + "__" + strconv.Itoa(pageParams.PageSize) + if pageParams != nil { + k += "__" + strconv.Itoa(pageParams.Page) + "__" + strconv.Itoa(pageParams.PageSize) + } return k } diff --git a/services/services.go b/services/services.go index c41b539..867e7bf 100644 --- a/services/services.go +++ b/services/services.go @@ -101,8 +101,11 @@ type ILeaderboardService interface { ScheduleDefault() Run([]*models.User, *models.IntervalKey, []uint8) error ExistsAnyByUser(string) (bool, error) + CountUsers() (int64, error) GetByInterval(*models.IntervalKey, *models.PageParams, bool) (models.Leaderboard, error) + GetByIntervalAndUser(*models.IntervalKey, string, bool) (models.Leaderboard, error) GetAggregatedByInterval(*models.IntervalKey, *uint8, *models.PageParams, bool) (models.Leaderboard, error) + GetAggregatedByIntervalAndUser(*models.IntervalKey, string, *uint8, bool) (models.Leaderboard, error) GenerateByUser(*models.User, *models.IntervalKey) (*models.LeaderboardItem, error) GenerateAggregatedByUser(*models.User, *models.IntervalKey, uint8) ([]*models.LeaderboardItem, error) } diff --git a/utils/http.go b/utils/http.go index 2edfad5..faebb7e 100644 --- a/utils/http.go +++ b/utils/http.go @@ -56,3 +56,14 @@ func ParsePageParams(r *http.Request) *models.PageParams { } return pageParams } + +func ParsePageParamsWithDefault(r *http.Request, page, size int) *models.PageParams { + pageParams := ParsePageParams(r) + if pageParams.Page == 0 { + pageParams.Page = page + } + if pageParams.PageSize == 0 { + pageParams.PageSize = size + } + return pageParams +}