chore: add maximum default leaderboard length

This commit is contained in:
Ferdinand Mütsch 2022-10-19 18:28:30 +02:00
parent ffbcfc7467
commit b1d7f87095
7 changed files with 124 additions and 9 deletions

View File

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

View File

@ -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 {

View File

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

View File

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

View File

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

View File

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

View File

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