Initial commit: VRBattles API
This commit is contained in:
669
src/handlers/players.rs
Normal file
669
src/handlers/players.rs
Normal file
@@ -0,0 +1,669 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
|
||||
use crate::{
|
||||
auth::AuthUser,
|
||||
error::{AppError, Result},
|
||||
};
|
||||
|
||||
use super::auth::AppState;
|
||||
|
||||
// ============================================================================
|
||||
// Request/Response Types
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct ListPlayersQuery {
|
||||
pub search: Option<String>,
|
||||
pub page: Option<i64>,
|
||||
pub per_page: Option<i64>,
|
||||
pub sort_by: Option<String>, // "mmr", "username", "date_registered"
|
||||
pub sort_order: Option<String>, // "asc", "desc"
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PlayerListResponse {
|
||||
pub players: Vec<PlayerSummary>,
|
||||
pub total: i64,
|
||||
pub page: i64,
|
||||
pub per_page: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
pub struct PlayerSummary {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub firstname: String,
|
||||
pub lastname: String,
|
||||
pub profile: Option<String>,
|
||||
pub date_registered: chrono::DateTime<chrono::Utc>,
|
||||
pub team_id: Option<i32>,
|
||||
pub team_name: Option<String>,
|
||||
pub team_position: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PlayerProfileResponse {
|
||||
pub id: i32,
|
||||
pub username: String,
|
||||
pub firstname: String,
|
||||
pub lastname: String,
|
||||
pub profile: Option<String>,
|
||||
pub date_registered: chrono::DateTime<chrono::Utc>,
|
||||
pub is_admin: bool,
|
||||
pub mu: f32,
|
||||
pub sigma: f32,
|
||||
pub ordinal: f32,
|
||||
pub mmr: f32,
|
||||
pub team: Option<PlayerTeamInfo>,
|
||||
pub stats: PlayerStats,
|
||||
pub recent_matches: Vec<PlayerMatchSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PlayerTeamInfo {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub logo: String,
|
||||
pub position: String,
|
||||
pub date_joined: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PlayerStats {
|
||||
pub matches_played: i64,
|
||||
pub matches_won: i64,
|
||||
pub matches_lost: i64,
|
||||
pub win_rate: f32,
|
||||
pub ladders_participated: i64,
|
||||
// OpenSkill rating stats
|
||||
pub mu: f64,
|
||||
pub sigma: f64,
|
||||
pub ordinal: f64,
|
||||
pub mmr: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
pub struct PlayerMatchSummary {
|
||||
pub match_id: i32,
|
||||
pub date_played: chrono::DateTime<chrono::Utc>,
|
||||
pub opponent_team_name: String,
|
||||
pub own_team_name: String,
|
||||
pub own_score: i32,
|
||||
pub opponent_score: i32,
|
||||
pub result: String, // "win", "loss", "draw"
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FeaturedPlayersResponse {
|
||||
pub players: Vec<FeaturedPlayerInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
pub struct FeaturedPlayerInfo {
|
||||
pub id: i32,
|
||||
pub player_id: i32,
|
||||
pub rank: i32,
|
||||
pub username: String,
|
||||
pub firstname: String,
|
||||
pub lastname: String,
|
||||
pub profile: Option<String>,
|
||||
pub team_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetFeaturedPlayerRequest {
|
||||
pub player_id: i32,
|
||||
pub rank: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct LeaderboardResponse {
|
||||
pub players: Vec<LeaderboardEntry>,
|
||||
pub total: i64,
|
||||
pub page: i64,
|
||||
pub per_page: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
pub struct LeaderboardEntry {
|
||||
pub rank: i64,
|
||||
pub player_id: i32,
|
||||
pub username: String,
|
||||
pub firstname: String,
|
||||
pub lastname: String,
|
||||
pub profile: Option<String>,
|
||||
pub team_name: Option<String>,
|
||||
pub matches_won: Option<i64>,
|
||||
pub matches_played: Option<i64>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Handlers
|
||||
// ============================================================================
|
||||
|
||||
/// GET /api/players
|
||||
/// List all players with pagination and search
|
||||
pub async fn list_players(
|
||||
Query(query): Query<ListPlayersQuery>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<PlayerListResponse>> {
|
||||
let page = query.page.unwrap_or(1).max(1);
|
||||
let per_page = query.per_page.unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * per_page;
|
||||
|
||||
let search_pattern = query.search.as_ref().map(|s| format!("%{}%", s));
|
||||
|
||||
// Determine sort order
|
||||
let sort_column = match query.sort_by.as_deref() {
|
||||
Some("username") => "u.username",
|
||||
Some("date_registered") => "u.date_registered",
|
||||
_ => "u.date_registered",
|
||||
};
|
||||
let sort_order = match query.sort_order.as_deref() {
|
||||
Some("asc") => "ASC",
|
||||
_ => "DESC",
|
||||
};
|
||||
|
||||
let (total, players): (i64, Vec<PlayerSummary>) = if let Some(ref pattern) = search_pattern {
|
||||
let total = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM users WHERE username ILIKE $1 OR firstname ILIKE $1 OR lastname ILIKE $1"
|
||||
)
|
||||
.bind(pattern)
|
||||
.fetch_one(&state.pool)
|
||||
.await?;
|
||||
|
||||
let query_str = format!(
|
||||
r#"
|
||||
SELECT u.id, u.username, u.firstname, u.lastname, u.profile, u.date_registered,
|
||||
tp.team_id, t.name as team_name, tp.position as team_position
|
||||
FROM users u
|
||||
LEFT JOIN team_players tp ON u.id = tp.player_id AND tp.status = 1
|
||||
LEFT JOIN teams t ON tp.team_id = t.id AND t.is_delete = false
|
||||
WHERE u.username ILIKE $1 OR u.firstname ILIKE $1 OR u.lastname ILIKE $1
|
||||
ORDER BY {} {}
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
sort_column, sort_order
|
||||
);
|
||||
|
||||
let players = sqlx::query_as(&query_str)
|
||||
.bind(pattern)
|
||||
.bind(per_page)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
(total, players)
|
||||
} else {
|
||||
let total = sqlx::query_scalar("SELECT COUNT(*) FROM users")
|
||||
.fetch_one(&state.pool)
|
||||
.await?;
|
||||
|
||||
let query_str = format!(
|
||||
r#"
|
||||
SELECT u.id, u.username, u.firstname, u.lastname, u.profile, u.date_registered,
|
||||
tp.team_id, t.name as team_name, tp.position as team_position
|
||||
FROM users u
|
||||
LEFT JOIN team_players tp ON u.id = tp.player_id AND tp.status = 1
|
||||
LEFT JOIN teams t ON tp.team_id = t.id AND t.is_delete = false
|
||||
ORDER BY {} {}
|
||||
LIMIT $1 OFFSET $2
|
||||
"#,
|
||||
sort_column, sort_order
|
||||
);
|
||||
|
||||
let players = sqlx::query_as(&query_str)
|
||||
.bind(per_page)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
(total, players)
|
||||
};
|
||||
|
||||
Ok(Json(PlayerListResponse {
|
||||
players,
|
||||
total,
|
||||
page,
|
||||
per_page,
|
||||
}))
|
||||
}
|
||||
|
||||
/// GET /api/players/:id
|
||||
/// Get player profile with stats
|
||||
pub async fn get_player(
|
||||
State(state): State<AppState>,
|
||||
Path(player_id): Path<i32>,
|
||||
) -> Result<Json<PlayerProfileResponse>> {
|
||||
// Get basic player info including OpenSkill ratings
|
||||
let player = sqlx::query_as::<_, (i32, String, String, String, Option<String>, chrono::DateTime<chrono::Utc>, bool, Option<f32>, Option<f32>, Option<f32>, Option<f32>)>(
|
||||
"SELECT id, username, firstname, lastname, profile, date_registered, is_admin, mu, sigma, ordinal, mmr FROM users WHERE id = $1"
|
||||
)
|
||||
.bind(player_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Player not found".to_string()))?;
|
||||
|
||||
// Get team info
|
||||
let team_info = sqlx::query_as::<_, (i32, String, String, String, chrono::DateTime<chrono::Utc>)>(
|
||||
r#"
|
||||
SELECT t.id, t.name, t.logo, tp.position, tp.date_created
|
||||
FROM team_players tp
|
||||
JOIN teams t ON tp.team_id = t.id
|
||||
WHERE tp.player_id = $1 AND tp.status = 1 AND t.is_delete = false
|
||||
"#
|
||||
)
|
||||
.bind(player_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
let team = team_info.map(|(id, name, logo, position, date_joined)| PlayerTeamInfo {
|
||||
id,
|
||||
name,
|
||||
logo,
|
||||
position,
|
||||
date_joined,
|
||||
});
|
||||
|
||||
// Get match stats
|
||||
let stats = get_player_stats(&state.pool, player_id, team.as_ref().map(|t| t.id)).await?;
|
||||
|
||||
// Get recent matches
|
||||
let recent_matches = if let Some(ref t) = team {
|
||||
get_recent_matches(&state.pool, t.id, 5).await?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(Json(PlayerProfileResponse {
|
||||
id: player.0,
|
||||
username: player.1,
|
||||
firstname: player.2,
|
||||
lastname: player.3,
|
||||
profile: player.4,
|
||||
date_registered: player.5,
|
||||
is_admin: player.6,
|
||||
mu: player.7.unwrap_or(25.0),
|
||||
sigma: player.8.unwrap_or(8.333),
|
||||
ordinal: player.9.unwrap_or(0.0),
|
||||
mmr: player.10.unwrap_or(1000.0),
|
||||
team,
|
||||
stats,
|
||||
recent_matches,
|
||||
}))
|
||||
}
|
||||
|
||||
/// GET /api/players/featured
|
||||
/// Get featured players
|
||||
pub async fn get_featured_players(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<FeaturedPlayersResponse>> {
|
||||
let players = sqlx::query_as::<_, FeaturedPlayerInfo>(
|
||||
r#"
|
||||
SELECT fp.id, fp.player_id, fp.rank, u.username, u.firstname, u.lastname, u.profile, t.name as team_name
|
||||
FROM featured_players fp
|
||||
JOIN users u ON fp.player_id = u.id
|
||||
LEFT JOIN team_players tp ON u.id = tp.player_id AND tp.status = 1
|
||||
LEFT JOIN teams t ON tp.team_id = t.id AND t.is_delete = false
|
||||
ORDER BY fp.rank ASC
|
||||
"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(Json(FeaturedPlayersResponse { players }))
|
||||
}
|
||||
|
||||
/// POST /api/players/featured
|
||||
/// Add/update featured player (admin only)
|
||||
pub async fn set_featured_player(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<SetFeaturedPlayerRequest>,
|
||||
) -> Result<(StatusCode, Json<FeaturedPlayerInfo>)> {
|
||||
if !user.is_admin {
|
||||
return Err(AppError::Forbidden("Only admins can manage featured players".to_string()));
|
||||
}
|
||||
|
||||
// Verify player exists
|
||||
let player_exists = sqlx::query_scalar::<_, i32>(
|
||||
"SELECT id FROM users WHERE id = $1"
|
||||
)
|
||||
.bind(payload.player_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
if player_exists.is_none() {
|
||||
return Err(AppError::NotFound("Player not found".to_string()));
|
||||
}
|
||||
|
||||
// Upsert featured player
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO featured_players (player_id, rank)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (player_id) DO UPDATE SET rank = $2
|
||||
"#
|
||||
)
|
||||
.bind(payload.player_id)
|
||||
.bind(payload.rank)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
// Fetch the full featured player info
|
||||
let featured = sqlx::query_as::<_, FeaturedPlayerInfo>(
|
||||
r#"
|
||||
SELECT fp.id, fp.player_id, fp.rank, u.username, u.firstname, u.lastname, u.profile, t.name as team_name
|
||||
FROM featured_players fp
|
||||
JOIN users u ON fp.player_id = u.id
|
||||
LEFT JOIN team_players tp ON u.id = tp.player_id AND tp.status = 1
|
||||
LEFT JOIN teams t ON tp.team_id = t.id AND t.is_delete = false
|
||||
WHERE fp.player_id = $1
|
||||
"#
|
||||
)
|
||||
.bind(payload.player_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok((StatusCode::CREATED, Json(featured)))
|
||||
}
|
||||
|
||||
/// DELETE /api/players/featured/:player_id
|
||||
/// Remove featured player (admin only)
|
||||
pub async fn remove_featured_player(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(player_id): Path<i32>,
|
||||
) -> Result<StatusCode> {
|
||||
if !user.is_admin {
|
||||
return Err(AppError::Forbidden("Only admins can manage featured players".to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query("DELETE FROM featured_players WHERE player_id = $1")
|
||||
.bind(player_id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound("Featured player not found".to_string()));
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// GET /api/players/leaderboard
|
||||
/// Get player leaderboard (by match wins)
|
||||
pub async fn get_leaderboard(
|
||||
Query(query): Query<ListPlayersQuery>,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<LeaderboardResponse>> {
|
||||
let page = query.page.unwrap_or(1).max(1);
|
||||
let per_page = query.per_page.unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * per_page;
|
||||
|
||||
// Count total players with at least one match
|
||||
let total: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT COUNT(DISTINCT u.id)
|
||||
FROM users u
|
||||
JOIN team_players tp ON u.id = tp.player_id AND tp.status = 1
|
||||
JOIN teams t ON tp.team_id = t.id AND t.is_delete = false
|
||||
JOIN (
|
||||
SELECT team_id_1 as team_id FROM matches WHERE matche_status = 3
|
||||
UNION ALL
|
||||
SELECT team_id_2 FROM matches WHERE matche_status = 3
|
||||
) m ON t.id = m.team_id
|
||||
"#
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
// Get leaderboard with match stats
|
||||
let players = sqlx::query_as::<_, LeaderboardEntry>(
|
||||
r#"
|
||||
WITH player_matches AS (
|
||||
SELECT
|
||||
u.id as player_id,
|
||||
u.username,
|
||||
u.firstname,
|
||||
u.lastname,
|
||||
u.profile,
|
||||
t.name as team_name,
|
||||
t.id as team_id
|
||||
FROM users u
|
||||
JOIN team_players tp ON u.id = tp.player_id AND tp.status = 1
|
||||
JOIN teams t ON tp.team_id = t.id AND t.is_delete = false
|
||||
),
|
||||
match_results AS (
|
||||
SELECT
|
||||
pm.player_id,
|
||||
COUNT(DISTINCT m.id) as matches_played,
|
||||
COUNT(DISTINCT CASE
|
||||
WHEN (m.team_id_1 = pm.team_id AND mr.team_1_score > mr.team_2_score)
|
||||
OR (m.team_id_2 = pm.team_id AND mr.team_2_score > mr.team_1_score)
|
||||
THEN m.id
|
||||
END) as matches_won
|
||||
FROM player_matches pm
|
||||
LEFT JOIN matches m ON (m.team_id_1 = pm.team_id OR m.team_id_2 = pm.team_id)
|
||||
AND m.matche_status = 3
|
||||
LEFT JOIN match_rounds mr ON m.id = mr.match_id AND mr.score_acceptance_status = 1
|
||||
GROUP BY pm.player_id
|
||||
)
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (ORDER BY COALESCE(mr.matches_won, 0) DESC, pm.username ASC) as rank,
|
||||
pm.player_id,
|
||||
pm.username,
|
||||
pm.firstname,
|
||||
pm.lastname,
|
||||
pm.profile,
|
||||
pm.team_name,
|
||||
mr.matches_won,
|
||||
mr.matches_played
|
||||
FROM player_matches pm
|
||||
LEFT JOIN match_results mr ON pm.player_id = mr.player_id
|
||||
ORDER BY COALESCE(mr.matches_won, 0) DESC, pm.username ASC
|
||||
LIMIT $1 OFFSET $2
|
||||
"#
|
||||
)
|
||||
.bind(per_page)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(Json(LeaderboardResponse {
|
||||
players,
|
||||
total,
|
||||
page,
|
||||
per_page,
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
async fn get_player_stats(
|
||||
pool: &sqlx::PgPool,
|
||||
player_id: i32,
|
||||
team_id: Option<i32>,
|
||||
) -> Result<PlayerStats> {
|
||||
// Get player's OpenSkill rating
|
||||
let rating: (Option<f32>, Option<f32>, Option<f32>, Option<f32>) = sqlx::query_as(
|
||||
"SELECT mu, sigma, ordinal, mmr FROM users WHERE id = $1"
|
||||
)
|
||||
.bind(player_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or((None, None, None, None));
|
||||
|
||||
let mu = rating.0.unwrap_or(25.0) as f64;
|
||||
let sigma = rating.1.unwrap_or(8.333333) as f64;
|
||||
let ordinal = rating.2.unwrap_or(0.0) as f64;
|
||||
let mmr = rating.3.unwrap_or(1500.0) as f64;
|
||||
|
||||
let Some(team_id) = team_id else {
|
||||
return Ok(PlayerStats {
|
||||
matches_played: 0,
|
||||
matches_won: 0,
|
||||
matches_lost: 0,
|
||||
win_rate: 0.0,
|
||||
ladders_participated: 0,
|
||||
mu,
|
||||
sigma,
|
||||
ordinal,
|
||||
mmr,
|
||||
});
|
||||
};
|
||||
|
||||
// Count completed matches
|
||||
let matches_played: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM matches
|
||||
WHERE (team_id_1 = $1 OR team_id_2 = $1) AND matche_status = 3
|
||||
"#
|
||||
)
|
||||
.bind(team_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
// Count wins (based on accepted scores)
|
||||
let matches_won: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT COUNT(DISTINCT m.id) FROM matches m
|
||||
JOIN match_rounds mr ON m.id = mr.match_id AND mr.score_acceptance_status = 1
|
||||
WHERE m.matche_status = 3
|
||||
AND (
|
||||
(m.team_id_1 = $1 AND mr.team_1_score > mr.team_2_score)
|
||||
OR (m.team_id_2 = $1 AND mr.team_2_score > mr.team_1_score)
|
||||
)
|
||||
"#
|
||||
)
|
||||
.bind(team_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let matches_lost = matches_played - matches_won;
|
||||
let win_rate = if matches_played > 0 {
|
||||
(matches_won as f32 / matches_played as f32) * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
// Count ladders participated
|
||||
let ladders_participated: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM ladder_teams WHERE team_id = $1"
|
||||
)
|
||||
.bind(team_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(PlayerStats {
|
||||
matches_played,
|
||||
matches_won,
|
||||
matches_lost,
|
||||
win_rate,
|
||||
ladders_participated,
|
||||
mu,
|
||||
sigma,
|
||||
ordinal,
|
||||
mmr,
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_recent_matches(
|
||||
pool: &sqlx::PgPool,
|
||||
team_id: i32,
|
||||
limit: i64,
|
||||
) -> Result<Vec<PlayerMatchSummary>> {
|
||||
let matches = sqlx::query_as::<_, PlayerMatchSummary>(
|
||||
r#"
|
||||
SELECT
|
||||
m.id as match_id,
|
||||
m.date_start as date_played,
|
||||
CASE WHEN m.team_id_1 = $1 THEN t2.name ELSE t1.name END as opponent_team_name,
|
||||
CASE WHEN m.team_id_1 = $1 THEN t1.name ELSE t2.name END as own_team_name,
|
||||
CASE WHEN m.team_id_1 = $1 THEN COALESCE(mr.team_1_score, 0) ELSE COALESCE(mr.team_2_score, 0) END as own_score,
|
||||
CASE WHEN m.team_id_1 = $1 THEN COALESCE(mr.team_2_score, 0) ELSE COALESCE(mr.team_1_score, 0) END as opponent_score,
|
||||
CASE
|
||||
WHEN mr.id IS NULL THEN 'pending'
|
||||
WHEN m.team_id_1 = $1 AND mr.team_1_score > mr.team_2_score THEN 'win'
|
||||
WHEN m.team_id_2 = $1 AND mr.team_2_score > mr.team_1_score THEN 'win'
|
||||
WHEN mr.team_1_score = mr.team_2_score THEN 'draw'
|
||||
ELSE 'loss'
|
||||
END as result
|
||||
FROM matches m
|
||||
JOIN teams t1 ON m.team_id_1 = t1.id
|
||||
JOIN teams t2 ON m.team_id_2 = t2.id
|
||||
LEFT JOIN match_rounds mr ON m.id = mr.match_id AND mr.score_acceptance_status = 1
|
||||
WHERE (m.team_id_1 = $1 OR m.team_id_2 = $1) AND m.matche_status = 3
|
||||
ORDER BY m.date_start DESC
|
||||
LIMIT $2
|
||||
"#
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(limit)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(matches)
|
||||
}
|
||||
|
||||
// Note: Profile upload is now handled by uploads.rs using multipart forms
|
||||
|
||||
/// GET /api/players/rating-history/:player_id
|
||||
/// Get a player's rating history
|
||||
pub async fn get_rating_history(
|
||||
State(state): State<AppState>,
|
||||
Path(player_id): Path<i32>,
|
||||
) -> Result<Json<Vec<RatingHistoryEntry>>> {
|
||||
let history = sqlx::query_as::<_, RatingHistoryEntry>(
|
||||
r#"
|
||||
SELECT
|
||||
rh.id, rh.date_created, rh.match_id,
|
||||
rh.old_mu, rh.old_sigma, rh.old_ordinal,
|
||||
rh.new_mu, rh.new_sigma, rh.new_ordinal,
|
||||
t1.name as opponent_name
|
||||
FROM rating_history rh
|
||||
LEFT JOIN matches m ON rh.match_id = m.id
|
||||
LEFT JOIN teams t1 ON (
|
||||
CASE
|
||||
WHEN m.team_id_1 IN (SELECT tp.team_id FROM team_players tp WHERE tp.player_id = $1 AND tp.status = 1)
|
||||
THEN m.team_id_2
|
||||
ELSE m.team_id_1
|
||||
END = t1.id
|
||||
)
|
||||
WHERE rh.entity_type = 'player' AND rh.entity_id = $1
|
||||
ORDER BY rh.date_created DESC
|
||||
LIMIT 50
|
||||
"#
|
||||
)
|
||||
.bind(player_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(Json(history))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
pub struct RatingHistoryEntry {
|
||||
pub id: i32,
|
||||
pub date_created: chrono::DateTime<chrono::Utc>,
|
||||
pub match_id: Option<i32>,
|
||||
pub old_mu: f64,
|
||||
pub old_sigma: f64,
|
||||
pub old_ordinal: f64,
|
||||
pub new_mu: f64,
|
||||
pub new_sigma: f64,
|
||||
pub new_ordinal: f64,
|
||||
pub opponent_name: Option<String>,
|
||||
}
|
||||
Reference in New Issue
Block a user