Implement leaderboard service and POC HTTP handlers with Swagger documentation

This commit is contained in:
2026-07-15 11:29:19 +04:00
parent c25e0bc581
commit 4a894a4399
10 changed files with 1300 additions and 3 deletions
+453
View File
@@ -0,0 +1,453 @@
package races
import (
"context"
"errors"
"fmt"
"time"
"github.com/jackc/pgx/v5"
)
// LeaderboardEntry is a single row in the leaderboard.
type LeaderboardEntry struct {
DriverID string
Name string
Nickname string
AvatarURL string
ClanID string
ClanTag string
Points int
BestPos *int // deprecated/optional
BestTimeMs *int64
Rank int
}
// LeaderboardResult groups the paginated items, total count, and current driver.
type LeaderboardResult struct {
Items []LeaderboardEntry
Total int
CurrentDriver *LeaderboardEntry
}
func getYearTimestamps(year int) (int64, int64) {
start := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(year+1, time.January, 1, 0, 0, 0, 0, time.UTC)
return start.UnixMilli(), end.UnixMilli()
}
// GetTrackLeaderboard returns track-specific leaderboard entries for the given track and year.
func (s *PgStore) GetTrackLeaderboard(ctx context.Context, trackID string, year int, limit, offset int) ([]LeaderboardEntry, int, error) {
startMs, endMs := getYearTimestamps(year)
if limit <= 0 {
limit = 50
}
countQuery := `
WITH best_results AS (
SELECT
rd.driver_id
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.track_id = $1
AND r.finished_ms >= $2
AND r.finished_ms < $3
GROUP BY rd.driver_id
)
SELECT COUNT(*) FROM best_results`
var total int
err := s.pool.QueryRow(ctx, countQuery, trackID, startMs, endMs).Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("count track leaderboard: %w", err)
}
if total == 0 {
return nil, 0, nil
}
query := `
WITH best_results AS (
SELECT
r.track_id,
rd.driver_id,
MIN(CASE WHEN rd.best_lap_ms > 0 THEN rd.best_lap_ms ELSE NULL END) as best_time
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY r.track_id, rd.driver_id
),
ranked_times AS (
SELECT
track_id,
driver_id,
best_time,
ROW_NUMBER() OVER (PARTITION BY track_id ORDER BY best_time ASC NULLS LAST, driver_id ASC) as time_rank
FROM best_results
),
track_points AS (
SELECT
track_id,
driver_id,
best_time,
CASE
WHEN best_time IS NULL THEN 1
WHEN time_rank <= 3 THEN 25
WHEN time_rank <= 10 THEN 18
WHEN time_rank <= 20 THEN 15
WHEN time_rank <= 30 THEN 12
WHEN time_rank <= 40 THEN 10
WHEN time_rank <= 50 THEN 8
WHEN time_rank <= 60 THEN 6
WHEN time_rank <= 70 THEN 4
WHEN time_rank <= 80 THEN 2
WHEN time_rank <= 90 THEN 1
ELSE 1
END as points
FROM ranked_times
),
ranked_leaderboard AS (
SELECT
tp.driver_id,
d.name,
d.nickname,
d.avatar_url,
COALESCE(d.clan_id, '') as clan_id,
COALESCE(c.tag, '') as clan_tag,
tp.points,
tp.best_time,
ROW_NUMBER() OVER (ORDER BY tp.points DESC, tp.best_time ASC NULLS LAST, tp.driver_id ASC) as rank
FROM track_points tp
JOIN drivers d ON tp.driver_id = d.id
LEFT JOIN clans c ON d.clan_id = c.id
WHERE tp.track_id = $3
)
SELECT rank, driver_id, name, nickname, avatar_url, clan_id, clan_tag, points, best_time
FROM ranked_leaderboard
ORDER BY rank ASC
LIMIT $4 OFFSET $5`
rows, err := s.pool.Query(ctx, query, startMs, endMs, trackID, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("query track leaderboard: %w", err)
}
defer rows.Close()
var entries []LeaderboardEntry
for rows.Next() {
var entry LeaderboardEntry
if err := rows.Scan(
&entry.Rank, &entry.DriverID, &entry.Name, &entry.Nickname,
&entry.AvatarURL, &entry.ClanID, &entry.ClanTag, &entry.Points, &entry.BestTimeMs,
); err != nil {
return nil, 0, err
}
entries = append(entries, entry)
}
return entries, total, nil
}
// GetTrackLeaderboardDriver returns a single driver's rank on a track.
func (s *PgStore) GetTrackLeaderboardDriver(ctx context.Context, trackID string, year int, driverID string) (*LeaderboardEntry, error) {
startMs, endMs := getYearTimestamps(year)
query := `
WITH best_results AS (
SELECT
r.track_id,
rd.driver_id,
MIN(CASE WHEN rd.best_lap_ms > 0 THEN rd.best_lap_ms ELSE NULL END) as best_time
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY r.track_id, rd.driver_id
),
ranked_times AS (
SELECT
track_id,
driver_id,
best_time,
ROW_NUMBER() OVER (PARTITION BY track_id ORDER BY best_time ASC NULLS LAST, driver_id ASC) as time_rank
FROM best_results
),
track_points AS (
SELECT
track_id,
driver_id,
best_time,
CASE
WHEN best_time IS NULL THEN 1
WHEN time_rank <= 3 THEN 25
WHEN time_rank <= 10 THEN 18
WHEN time_rank <= 20 THEN 15
WHEN time_rank <= 30 THEN 12
WHEN time_rank <= 40 THEN 10
WHEN time_rank <= 50 THEN 8
WHEN time_rank <= 60 THEN 6
WHEN time_rank <= 70 THEN 4
WHEN time_rank <= 80 THEN 2
WHEN time_rank <= 90 THEN 1
ELSE 1
END as points
FROM ranked_times
),
ranked_leaderboard AS (
SELECT
tp.driver_id,
d.name,
d.nickname,
d.avatar_url,
COALESCE(d.clan_id, '') as clan_id,
COALESCE(c.tag, '') as clan_tag,
tp.points,
tp.best_time,
ROW_NUMBER() OVER (ORDER BY tp.points DESC, tp.best_time ASC NULLS LAST, tp.driver_id ASC) as rank
FROM track_points tp
JOIN drivers d ON tp.driver_id = d.id
LEFT JOIN clans c ON d.clan_id = c.id
WHERE tp.track_id = $3
)
SELECT rank, driver_id, name, nickname, avatar_url, clan_id, clan_tag, points, best_time
FROM ranked_leaderboard
WHERE driver_id = $4`
row := s.pool.QueryRow(ctx, query, startMs, endMs, trackID, driverID)
var entry LeaderboardEntry
if err := row.Scan(
&entry.Rank, &entry.DriverID, &entry.Name, &entry.Nickname,
&entry.AvatarURL, &entry.ClanID, &entry.ClanTag, &entry.Points, &entry.BestTimeMs,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &entry, nil
}
// GetOverallLeaderboard returns overall leaderboard entries for the given year.
func (s *PgStore) GetOverallLeaderboard(ctx context.Context, year int, limit, offset int) ([]LeaderboardEntry, int, error) {
startMs, endMs := getYearTimestamps(year)
if limit <= 0 {
limit = 50
}
countQuery := `
WITH best_results AS (
SELECT
rd.driver_id
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY rd.driver_id
)
SELECT COUNT(*) FROM best_results`
var total int
err := s.pool.QueryRow(ctx, countQuery, startMs, endMs).Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("count overall leaderboard: %w", err)
}
if total == 0 {
return nil, 0, nil
}
query := `
WITH best_results AS (
SELECT
r.track_id,
rd.driver_id,
MIN(CASE WHEN rd.best_lap_ms > 0 THEN rd.best_lap_ms ELSE NULL END) as best_time
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY r.track_id, rd.driver_id
),
ranked_times AS (
SELECT
track_id,
driver_id,
best_time,
ROW_NUMBER() OVER (PARTITION BY track_id ORDER BY best_time ASC NULLS LAST, driver_id ASC) as time_rank
FROM best_results
),
track_points AS (
SELECT
track_id,
driver_id,
best_time,
CASE
WHEN best_time IS NULL THEN 1
WHEN time_rank <= 3 THEN 25
WHEN time_rank <= 10 THEN 18
WHEN time_rank <= 20 THEN 15
WHEN time_rank <= 30 THEN 12
WHEN time_rank <= 40 THEN 10
WHEN time_rank <= 50 THEN 8
WHEN time_rank <= 60 THEN 6
WHEN time_rank <= 70 THEN 4
WHEN time_rank <= 80 THEN 2
WHEN time_rank <= 90 THEN 1
ELSE 1
END as points
FROM ranked_times
),
overall_points AS (
SELECT
driver_id,
SUM(points) as points
FROM track_points
GROUP BY driver_id
),
ranked_overall AS (
SELECT
op.driver_id,
d.name,
d.nickname,
d.avatar_url,
COALESCE(d.clan_id, '') as clan_id,
COALESCE(c.tag, '') as clan_tag,
op.points,
ROW_NUMBER() OVER (ORDER BY op.points DESC, op.driver_id ASC) as rank
FROM overall_points op
JOIN drivers d ON op.driver_id = d.id
LEFT JOIN clans c ON d.clan_id = c.id
)
SELECT rank, driver_id, name, nickname, avatar_url, clan_id, clan_tag, points
FROM ranked_overall
ORDER BY rank ASC
LIMIT $3 OFFSET $4`
rows, err := s.pool.Query(ctx, query, startMs, endMs, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("query overall leaderboard: %w", err)
}
defer rows.Close()
var entries []LeaderboardEntry
for rows.Next() {
var entry LeaderboardEntry
if err := rows.Scan(
&entry.Rank, &entry.DriverID, &entry.Name, &entry.Nickname,
&entry.AvatarURL, &entry.ClanID, &entry.ClanTag, &entry.Points,
); err != nil {
return nil, 0, err
}
entries = append(entries, entry)
}
return entries, total, nil
}
// GetOverallLeaderboardDriver returns a single driver's overall rank.
func (s *PgStore) GetOverallLeaderboardDriver(ctx context.Context, year int, driverID string) (*LeaderboardEntry, error) {
startMs, endMs := getYearTimestamps(year)
query := `
WITH best_results AS (
SELECT
r.track_id,
rd.driver_id,
MIN(CASE WHEN rd.best_lap_ms > 0 THEN rd.best_lap_ms ELSE NULL END) as best_time
FROM race_drivers rd
JOIN races r ON rd.race_id = r.id
WHERE r.status = 'finished'
AND r.finished_ms >= $1
AND r.finished_ms < $2
GROUP BY r.track_id, rd.driver_id
),
ranked_times AS (
SELECT
track_id,
driver_id,
best_time,
ROW_NUMBER() OVER (PARTITION BY track_id ORDER BY best_time ASC NULLS LAST, driver_id ASC) as time_rank
FROM best_results
),
track_points AS (
SELECT
track_id,
driver_id,
best_time,
CASE
WHEN best_time IS NULL THEN 1
WHEN time_rank <= 3 THEN 25
WHEN time_rank <= 10 THEN 18
WHEN time_rank <= 20 THEN 15
WHEN time_rank <= 30 THEN 12
WHEN time_rank <= 40 THEN 10
WHEN time_rank <= 50 THEN 8
WHEN time_rank <= 60 THEN 6
WHEN time_rank <= 70 THEN 4
WHEN time_rank <= 80 THEN 2
WHEN time_rank <= 90 THEN 1
ELSE 1
END as points
FROM ranked_times
),
overall_points AS (
SELECT
driver_id,
SUM(points) as points
FROM track_points
GROUP BY driver_id
),
ranked_overall AS (
SELECT
op.driver_id,
d.name,
d.nickname,
d.avatar_url,
COALESCE(d.clan_id, '') as clan_id,
COALESCE(c.tag, '') as clan_tag,
op.points,
ROW_NUMBER() OVER (ORDER BY op.points DESC, op.driver_id ASC) as rank
FROM overall_points op
JOIN drivers d ON op.driver_id = d.id
LEFT JOIN clans c ON d.clan_id = c.id
)
SELECT rank, driver_id, name, nickname, avatar_url, clan_id, clan_tag, points
FROM ranked_overall
WHERE driver_id = $3`
row := s.pool.QueryRow(ctx, query, startMs, endMs, driverID)
var entry LeaderboardEntry
if err := row.Scan(
&entry.Rank, &entry.DriverID, &entry.Name, &entry.Nickname,
&entry.AvatarURL, &entry.ClanID, &entry.ClanTag, &entry.Points,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &entry, nil
}
// GetDriverLeaderboardProfile returns driver profile information.
func (s *PgStore) GetDriverLeaderboardProfile(ctx context.Context, driverID string) (*LeaderboardEntry, error) {
query := `
SELECT d.id, d.name, d.nickname, d.avatar_url, COALESCE(d.clan_id, ''), COALESCE(c.tag, '')
FROM drivers d
LEFT JOIN clans c ON d.clan_id = c.id
WHERE d.id = $1`
row := s.pool.QueryRow(ctx, query, driverID)
var entry LeaderboardEntry
if err := row.Scan(
&entry.DriverID, &entry.Name, &entry.Nickname, &entry.AvatarURL, &entry.ClanID, &entry.ClanTag,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &entry, nil
}