feat(backend): leaderboard tiers, N+1 fix, contributors, certificate approval, physical disable
- leaderboard: batch-fetch users (GetUsersByIDs), deterministic tie-break by user_id - leaderboard: tier system (champion/gold/silver/bronze) based on rank percentile - store: HasCompletedCurriculum requires cert.Status == "issued"; pending does not satisfy - certificate: issueCertificateIfEligible creates pending row; admin approves via handleAdminIssueCertificate - certificate: handleRequestPhysicalCertificate returns 410 Gone (feature disabled) - settings: contributors CRUD (loadContributors, validateContributors, handleAdminContributorAvatarUpload) - community: expose contributors in /api/community response - rate_limit: separate DevMode bypass from redis-nil check in allowWSHandshake - router: remove /api/search route; fix missing tab indent on /api/meta line - migration 0032: contributors app_setting with seed entry
This commit is contained in:
@@ -148,6 +148,27 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (*models.User, error)
|
||||
return u, err
|
||||
}
|
||||
|
||||
// GetUsersByIDs fetches multiple users in a single query. Missing IDs are silently skipped.
|
||||
func (s *Store) GetUsersByIDs(ctx context.Context, ids []int64) (map[int64]*models.User, error) {
|
||||
if len(ids) == 0 {
|
||||
return map[int64]*models.User{}, nil
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `SELECT `+userCols+` FROM users WHERE id = ANY($1)`, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make(map[int64]*models.User, len(ids))
|
||||
for rows.Next() {
|
||||
u, err := scanUser(rows)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out[u.ID] = u
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) UpdateUserStats(ctx context.Context, userID int64, xp int, streak int, streakLongest int, lastActivity *time.Time) error {
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
UPDATE users SET xp=$1, streak_current=$2, streak_longest=$3, last_activity_date=$4
|
||||
@@ -634,7 +655,8 @@ func (s *Store) CountPassedActiveLessons(ctx context.Context, userID int64) (int
|
||||
func (s *Store) HasCompletedCurriculum(ctx context.Context, userID int64) (bool, error) {
|
||||
// An issued (non-revoked) certificate is permanent proof of completion regardless
|
||||
// of subsequent curriculum changes (e.g. new lessons added after the student finished).
|
||||
if cert, err := s.GetCertificateByUser(ctx, userID); err == nil && cert.RevokedAt == nil {
|
||||
// Pending certs do NOT count — they haven't been approved yet.
|
||||
if cert, err := s.GetCertificateByUser(ctx, userID); err == nil && cert.RevokedAt == nil && cert.Status == "issued" {
|
||||
return true, nil
|
||||
}
|
||||
active, err := s.CountActiveLessons(ctx)
|
||||
|
||||
Reference in New Issue
Block a user