mirror of
https://github.com/IS1DI/x0gp.git
synced 2026-07-16 20:47:55 +00:00
498 lines
13 KiB
Go
498 lines
13 KiB
Go
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
|
|
}
|
|
|
|
// TrackCalendarInfo holds the basic details of a scheduled track.
|
|
type TrackCalendarInfo struct {
|
|
ID int
|
|
TrackID string
|
|
TrackName string
|
|
StartAt time.Time
|
|
EndAt time.Time
|
|
}
|
|
|
|
// TrackCalendarLeaderboard holds the leaderboard summary for a track calendar event.
|
|
type TrackCalendarLeaderboard struct {
|
|
TrackID string
|
|
TrackName string
|
|
StartAt time.Time
|
|
EndAt time.Time
|
|
Status string // finished, active, planned
|
|
Podium []LeaderboardEntry
|
|
CurrentDriver *LeaderboardEntry
|
|
}
|
|
|
|
// GetCalendarLeaderboardTracks returns all track calendar entries joined with their track names, sorted chronologically.
|
|
func (s *PgStore) GetCalendarLeaderboardTracks(ctx context.Context) ([]TrackCalendarInfo, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT tc.id, tc.track_id, t.name, tc.start_at, tc.end_at
|
|
FROM track_calendar tc
|
|
JOIN tracks t ON tc.track_id = t.id
|
|
ORDER BY tc.start_at DESC
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query calendar tracks: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var out []TrackCalendarInfo
|
|
for rows.Next() {
|
|
var info TrackCalendarInfo
|
|
if err := rows.Scan(&info.ID, &info.TrackID, &info.TrackName, &info.StartAt, &info.EndAt); err != nil {
|
|
return nil, fmt.Errorf("scan calendar track: %w", err)
|
|
}
|
|
out = append(out, info)
|
|
}
|
|
return out, nil
|
|
}
|