uploaded
This commit is contained in:
176
server/routes/rankings.js
Normal file
176
server/routes/rankings.js
Normal file
@@ -0,0 +1,176 @@
|
||||
const express = require("express");
|
||||
const { pool } = require("../db");
|
||||
const { GAMEMODES, avatarUrl, normalizeGamemode } = require("../config");
|
||||
const { ValidationError, validateRequest } = require("../middleware/validation");
|
||||
const { sendError } = require("../utils/http");
|
||||
|
||||
const router = express.Router();
|
||||
const PER_PAGE = 20;
|
||||
|
||||
const TIER_POINTS_SQL = `
|
||||
CASE pr.tier
|
||||
WHEN 'HT1' THEN 60
|
||||
WHEN 'LT1' THEN 45
|
||||
WHEN 'HT2' THEN 30
|
||||
WHEN 'LT2' THEN 20
|
||||
WHEN 'HT3' THEN 10
|
||||
WHEN 'LT3' THEN 6
|
||||
WHEN 'HT4' THEN 4
|
||||
WHEN 'LT4' THEN 3
|
||||
WHEN 'HT5' THEN 2
|
||||
WHEN 'LT5' THEN 1
|
||||
ELSE 0
|
||||
END`;
|
||||
|
||||
const GM_POINTS_SQL = `
|
||||
CASE gm.tier
|
||||
WHEN 'HT1' THEN 60
|
||||
WHEN 'LT1' THEN 45
|
||||
WHEN 'HT2' THEN 30
|
||||
WHEN 'LT2' THEN 20
|
||||
WHEN 'HT3' THEN 10
|
||||
WHEN 'LT3' THEN 6
|
||||
WHEN 'HT4' THEN 4
|
||||
WHEN 'LT4' THEN 3
|
||||
WHEN 'HT5' THEN 2
|
||||
WHEN 'LT5' THEN 1
|
||||
ELSE 0
|
||||
END`;
|
||||
|
||||
const schema = {
|
||||
params: {
|
||||
fields: {
|
||||
gamemode: {
|
||||
type: "string",
|
||||
required: true,
|
||||
minLength: 3,
|
||||
maxLength: 32,
|
||||
toLowerCase: true,
|
||||
},
|
||||
},
|
||||
allowUnknown: false,
|
||||
},
|
||||
query: {
|
||||
fields: {
|
||||
page: { type: "int", min: 1, max: 50000 },
|
||||
perPage: { type: "int", min: 1, max: 500 },
|
||||
},
|
||||
allowUnknown: false,
|
||||
},
|
||||
};
|
||||
|
||||
function validateOrReply(req, res) {
|
||||
try {
|
||||
return validateRequest(req, schema);
|
||||
} catch (error) {
|
||||
if (error instanceof ValidationError) {
|
||||
sendError(res, 400, "VALIDATION_ERROR", error.message, error.details);
|
||||
return null;
|
||||
}
|
||||
|
||||
sendError(res, 400, "VALIDATION_ERROR", "Invalid request payload.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function buildDefaultTiers() {
|
||||
return Object.fromEntries(GAMEMODES.map((mode) => [mode, null]));
|
||||
}
|
||||
|
||||
router.get("/rankings/:gamemode", async (req, res) => {
|
||||
const validated = validateOrReply(req, res);
|
||||
if (!validated) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const gamemode = normalizeGamemode(validated.params.gamemode);
|
||||
const page = validated.query.page || 1;
|
||||
const perPage = validated.query.perPage || PER_PAGE;
|
||||
|
||||
if (!GAMEMODES.includes(gamemode)) {
|
||||
return sendError(res, 400, "VALIDATION_ERROR", "Invalid gamemode");
|
||||
}
|
||||
|
||||
const [countRows] = await pool.query(
|
||||
"SELECT COUNT(*) AS total FROM player_ranks WHERE gamemode = ?",
|
||||
[gamemode]
|
||||
);
|
||||
|
||||
const total = Number(countRows[0]?.total || 0);
|
||||
const totalPages = Math.max(Math.ceil(total / perPage), 1);
|
||||
const start = (page - 1) * perPage;
|
||||
|
||||
if (total === 0) {
|
||||
return res.json({
|
||||
gamemode,
|
||||
data: [],
|
||||
pagination: { page, perPage, total, totalPages },
|
||||
});
|
||||
}
|
||||
|
||||
const [pageRows] = await pool.query(
|
||||
`SELECT
|
||||
p.id,
|
||||
p.username,
|
||||
p.region,
|
||||
gm.tier AS gm_tier,
|
||||
COALESCE(SUM(${TIER_POINTS_SQL}), 0) AS total_points,
|
||||
${GM_POINTS_SQL} AS gamemode_points
|
||||
FROM players p
|
||||
JOIN player_ranks gm ON gm.player_id = p.id AND gm.gamemode = ?
|
||||
LEFT JOIN player_ranks pr ON pr.player_id = p.id
|
||||
GROUP BY p.id, p.username, p.region, gm.tier
|
||||
ORDER BY gamemode_points DESC, total_points DESC, p.username ASC
|
||||
LIMIT ? OFFSET ?`,
|
||||
[gamemode, perPage, start]
|
||||
);
|
||||
|
||||
const playerIds = pageRows.map((row) => row.id);
|
||||
const [allTierRows] = playerIds.length
|
||||
? await pool.query(
|
||||
"SELECT player_id, gamemode, tier FROM player_ranks WHERE player_id IN (?)",
|
||||
[playerIds]
|
||||
)
|
||||
: [[]];
|
||||
|
||||
const byId = new Map();
|
||||
for (let i = 0; i < pageRows.length; i += 1) {
|
||||
const row = pageRows[i];
|
||||
byId.set(row.id, {
|
||||
id: row.id,
|
||||
username: row.username,
|
||||
region: row.region,
|
||||
avatarUrl: avatarUrl(row.username),
|
||||
tiers: buildDefaultTiers(),
|
||||
totalPoints: Number(row.total_points || 0),
|
||||
gamemodePoints: Number(row.gamemode_points || 0),
|
||||
position: start + i + 1,
|
||||
});
|
||||
}
|
||||
|
||||
for (const row of allTierRows) {
|
||||
const target = byId.get(row.player_id);
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
target.tiers[row.gamemode] = row.tier;
|
||||
}
|
||||
|
||||
return res.json({
|
||||
gamemode,
|
||||
data: Array.from(byId.values()),
|
||||
pagination: {
|
||||
page,
|
||||
perPage,
|
||||
total,
|
||||
totalPages,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("GET /api/rankings/:gamemode failed:", error?.message || error);
|
||||
return sendError(res, 500, "RANKINGS_FETCH_FAILED", "Failed to fetch rankings");
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user