diff --git a/server/src/controllers/ladderController.js b/server/src/controllers/ladderController.js index 56ebf2ca..8ab441ed 100644 --- a/server/src/controllers/ladderController.js +++ b/server/src/controllers/ladderController.js @@ -1,28 +1,46 @@ -const { LadderUser, User, Store } = require('../models'); -const { LADDER_LEVEL_NAMES, LADDER_LEVEL_DESC, POWER_CALC } = require('../config/constants'); -const { success, error, getPagination, pageResult } = require('../utils/helper'); -const { Op } = require('sequelize'); -const sequelize = require('../config/database'); +const { LadderUser, User, Store } = require("../models"); +const { + LADDER_LEVEL_NAMES, + LADDER_LEVEL_DESC, + POWER_CALC, +} = require("../config/constants"); +const { + success, + error, + getPagination, + pageResult, +} = require("../utils/helper"); +const { Op } = require("sequelize"); +const sequelize = require("../config/database"); class LadderController { // 获取天梯排名 async getRanking(req, res) { try { - const { store_id, gender, level, page = 1, pageSize = 50, is_display } = req.query; + const { + store_id, + gender, + level, + page = 1, + pageSize = 50, + is_display, + } = req.query; const { limit, offset } = getPagination(page, pageSize); if (!store_id) { - return res.status(400).json(error('缺少门店ID', 400)); + return res.status(400).json(error("缺少门店ID", 400)); } const where = { store_id, - status: 1 + status: 1, }; // 如果不是大屏显示,则需要满足每月最低参赛场次限制 if (!is_display) { - where.monthly_match_count = { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES }; + where.monthly_match_count = { + [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES, + }; } if (gender) { @@ -35,11 +53,11 @@ class LadderController { const { rows, count } = await LadderUser.findAndCountAll({ where, include: [ - { model: User, as: 'user', attributes: ['nickname', 'avatar'] } + { model: User, as: "user", attributes: ["nickname", "avatar"] }, ], - order: [['power_score', 'DESC']], + order: [["power_score", "DESC"]], limit, - offset + offset, }); // 添加排名 @@ -57,13 +75,16 @@ class LadderController { powerScore: lu.power_score, matchCount: lu.match_count, winCount: lu.win_count, - winRate: lu.match_count > 0 ? Math.round(lu.win_count / lu.match_count * 100) : 0 + winRate: + lu.match_count > 0 + ? Math.round((lu.win_count / lu.match_count) * 100) + : 0, })); res.json(pageResult(list, count, page, pageSize)); } catch (err) { - console.error('获取排名失败:', err); - res.status(500).json(error('获取失败')); + console.error("获取排名失败:", err); + res.status(500).json(error("获取失败")); } } @@ -74,13 +95,17 @@ class LadderController { const ladderUser = await LadderUser.findByPk(id, { include: [ - { model: User, as: 'user', attributes: ['nickname', 'avatar', 'member_code'] }, - { model: Store, as: 'store', attributes: ['id', 'name'] } - ] + { + model: User, + as: "user", + attributes: ["nickname", "avatar", "member_code"], + }, + { model: Store, as: "store", attributes: ["id", "name"] }, + ], }); if (!ladderUser || ladderUser.status !== 1) { - return res.status(404).json(error('用户不存在', 404)); + return res.status(404).json(error("用户不存在", 404)); } // 计算排名 @@ -90,62 +115,138 @@ class LadderController { gender: ladderUser.gender, status: 1, power_score: { [Op.gt]: ladderUser.power_score }, - monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES } - } + monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES }, + }, }); - res.json(success({ - id: ladderUser.id, - userId: ladderUser.user_id, - realName: ladderUser.real_name, - nickname: ladderUser.user?.nickname, - avatar: ladderUser.user?.avatar, - memberCode: ladderUser.user?.member_code, - gender: ladderUser.gender, - level: ladderUser.level, - levelName: LADDER_LEVEL_NAMES[ladderUser.level], - levelDesc: LADDER_LEVEL_DESC[ladderUser.level], - powerScore: ladderUser.power_score, - matchCount: ladderUser.match_count, - winCount: ladderUser.win_count, - monthlyMatchCount: ladderUser.monthly_match_count, - winRate: ladderUser.match_count > 0 ? Math.round(ladderUser.win_count / ladderUser.match_count * 100) : 0, - rank: higherCount + 1, - storeId: ladderUser.store_id, - storeName: ladderUser.store?.name, - lastMatchTime: ladderUser.last_match_time - })); + res.json( + success({ + id: ladderUser.id, + userId: ladderUser.user_id, + realName: ladderUser.real_name, + nickname: ladderUser.user?.nickname, + avatar: ladderUser.user?.avatar, + memberCode: ladderUser.user?.member_code, + gender: ladderUser.gender, + level: ladderUser.level, + levelName: LADDER_LEVEL_NAMES[ladderUser.level], + levelDesc: LADDER_LEVEL_DESC[ladderUser.level], + powerScore: ladderUser.power_score, + matchCount: ladderUser.match_count, + winCount: ladderUser.win_count, + monthlyMatchCount: ladderUser.monthly_match_count, + winRate: + ladderUser.match_count > 0 + ? Math.round( + (ladderUser.win_count / ladderUser.match_count) * 100, + ) + : 0, + rank: higherCount + 1, + storeId: ladderUser.store_id, + storeName: ladderUser.store?.name, + lastMatchTime: ladderUser.last_match_time, + }), + ); } catch (err) { - console.error('获取用户详情失败:', err); - res.status(500).json(error('获取失败')); + console.error("获取用户详情失败:", err); + res.status(500).json(error("获取失败")); + } + } + + // 选手详情(兼容小程序端:/api/ladder/player?id=xxx) + async getPlayerDetail(req, res) { + try { + const id = req.query && req.query.id ? String(req.query.id) : null; + if (!id) { + return res.status(400).json(error("缺少选手ID", 400)); + } + + const ladderUser = await LadderUser.findByPk(id, { + include: [ + { + model: User, + as: "user", + attributes: ["nickname", "avatar", "member_code"], + }, + { model: Store, as: "store", attributes: ["id", "name"] }, + ], + }); + + if (!ladderUser || ladderUser.status !== 1) { + return res.status(404).json(error("用户不存在", 404)); + } + + const higherCount = await LadderUser.count({ + where: { + store_id: ladderUser.store_id, + gender: ladderUser.gender, + status: 1, + power_score: { [Op.gt]: ladderUser.power_score }, + monthly_match_count: { [Op.gte]: POWER_CALC.MIN_MONTHLY_MATCHES }, + }, + }); + + const matchCount = ladderUser.match_count || 0; + const winCount = ladderUser.win_count || 0; + + res.json( + success({ + id: ladderUser.id, + userId: ladderUser.user_id, + realName: ladderUser.real_name, + nickname: ladderUser.user && ladderUser.user.nickname, + avatar: ladderUser.user && ladderUser.user.avatar, + memberCode: ladderUser.user && ladderUser.user.member_code, + gender: ladderUser.gender, + level: ladderUser.level, + levelName: LADDER_LEVEL_NAMES[ladderUser.level], + levelDesc: LADDER_LEVEL_DESC[ladderUser.level], + powerScore: ladderUser.power_score, + matchCount: matchCount, + winCount: winCount, + loseCount: Math.max(matchCount - winCount, 0), + monthlyMatchCount: ladderUser.monthly_match_count, + winRate: + matchCount > 0 ? Math.round((winCount / matchCount) * 100) : 0, + rank: higherCount + 1, + storeId: ladderUser.store_id, + storeName: ladderUser.store && ladderUser.store.name, + lastMatchTime: ladderUser.last_match_time, + }), + ); + } catch (err) { + console.error("获取用户详情失败:", err); + res.status(500).json(error("获取失败")); } } // 获取等级说明 async getLevelInfo(req, res) { try { - const levels = Object.keys(LADDER_LEVEL_NAMES).map(level => ({ + const levels = Object.keys(LADDER_LEVEL_NAMES).map((level) => ({ level: parseInt(level), name: LADDER_LEVEL_NAMES[level], - description: LADDER_LEVEL_DESC[level] + description: LADDER_LEVEL_DESC[level], })); - res.json(success({ - levels, - powerCalcRules: { - baseWin: POWER_CALC.BASE_WIN, - baseLose: POWER_CALC.BASE_LOSE, - underdogThreshold: POWER_CALC.UNDERDOG_THRESHOLD, - underdogRate: POWER_CALC.UNDERDOG_RATE, - maxChange: POWER_CALC.MAX_CHANGE, - newbieProtection: POWER_CALC.NEWBIE_PROTECTION, - minMonthlyMatches: POWER_CALC.MIN_MONTHLY_MATCHES, - challengeCooldown: POWER_CALC.CHALLENGE_COOLDOWN - } - })); + res.json( + success({ + levels, + powerCalcRules: { + baseWin: POWER_CALC.BASE_WIN, + baseLose: POWER_CALC.BASE_LOSE, + underdogThreshold: POWER_CALC.UNDERDOG_THRESHOLD, + underdogRate: POWER_CALC.UNDERDOG_RATE, + maxChange: POWER_CALC.MAX_CHANGE, + newbieProtection: POWER_CALC.NEWBIE_PROTECTION, + minMonthlyMatches: POWER_CALC.MIN_MONTHLY_MATCHES, + challengeCooldown: POWER_CALC.CHALLENGE_COOLDOWN, + }, + }), + ); } catch (err) { - console.error('获取等级信息失败:', err); - res.status(500).json(error('获取失败')); + console.error("获取等级信息失败:", err); + res.status(500).json(error("获取失败")); } } } diff --git a/server/src/controllers/matchController.js b/server/src/controllers/matchController.js index 79433a02..cd9528cf 100644 --- a/server/src/controllers/matchController.js +++ b/server/src/controllers/matchController.js @@ -756,6 +756,90 @@ class MatchController { } } + // 获取选手比赛记录(用于选手详情页) + async getPlayerHistory(req, res) { + try { + const { player_id, page = 1, pageSize = 20 } = req.query; + const { limit, offset } = getPagination(page, pageSize); + + if (!player_id) { + return res.status(400).json(error('缺少选手ID', 400)); + } + + const player = await LadderUser.findByPk(player_id); + if (!player || player.status !== 1) { + return res.json(pageResult([], 0, page, pageSize)); + } + + const { rows, count } = await MatchGame.findAndCountAll({ + where: { + [Op.or]: [ + { player1_id: player.id }, + { player2_id: player.id } + ], + confirm_status: CONFIRM_STATUS.CONFIRMED + }, + include: [ + { model: Match, as: 'match', attributes: ['id', 'name', 'type', 'weight'] } + ], + order: [['confirmed_at', 'DESC']], + limit, + offset + }); + + const opponentIds = []; + rows.forEach((g) => { + if (g.player1_id === player.id) opponentIds.push(g.player2_id); + else opponentIds.push(g.player1_id); + }); + const uniqueOpponentIds = Array.from(new Set(opponentIds)); + const opponents = await LadderUser.findAll({ + where: { id: { [Op.in]: uniqueOpponentIds } }, + include: [{ model: User, as: 'user', attributes: ['nickname', 'avatar'] }] + }); + const opponentMap = new Map(opponents.map((o) => [String(o.id), o])); + + const list = rows.map((game) => { + const isPlayer1 = game.player1_id === player.id; + const opponentId = isPlayer1 ? game.player2_id : game.player1_id; + const opponent = opponentMap.get(String(opponentId)); + + const myScore = isPlayer1 ? game.player1_score : game.player2_score; + const opponentScore = isPlayer1 ? game.player2_score : game.player1_score; + const isWin = game.winner_id === player.id; + const typeName = game.match && game.match.type === MATCH_TYPES.CHALLENGE ? '挑战赛' : '排位赛'; + + return { + id: game.id, + matchId: game.match_id, + name: (game.match && game.match.name) || typeName, + type: game.match && game.match.type, + typeName, + createTime: game.confirmed_at, + desc: opponent ? `vs ${opponent.real_name} ${myScore}:${opponentScore}` : `${myScore}:${opponentScore}`, + result: isWin ? 'win' : 'lose', + resultName: isWin ? '胜' : '负', + powerChange: isWin ? game.winner_power_change : game.loser_power_change, + opponent: opponent + ? { + id: opponent.id, + realName: opponent.real_name, + nickname: opponent.user && opponent.user.nickname, + avatar: opponent.user && opponent.user.avatar, + level: opponent.level, + powerScore: opponent.power_score + } + : null + }; + }); + + res.json(pageResult(list, count, page, pageSize)); + } catch (err) { + console.error('获取选手比赛记录失败:', err); + res.status(500).json(error('获取失败')); + } + } + // 获取正在进行中的比赛 async getOngoingMatches(req, res) { try { diff --git a/server/src/routes/ladder.js b/server/src/routes/ladder.js index b9d97191..623fb7fd 100644 --- a/server/src/routes/ladder.js +++ b/server/src/routes/ladder.js @@ -9,6 +9,9 @@ router.get('/ranking', ladderController.getRanking); // 获取天梯用户详情 router.get('/user/:id', ladderController.getUserDetail); +// 选手详情(兼容小程序端:/api/ladder/player?id=xxx) +router.get('/player', ladderController.getPlayerDetail); + // 获取等级说明 router.get('/levels', ladderController.getLevelInfo); diff --git a/server/src/routes/match.js b/server/src/routes/match.js index 370f779f..97bae539 100644 --- a/server/src/routes/match.js +++ b/server/src/routes/match.js @@ -39,6 +39,9 @@ router.post('/ranking/confirm-score', authUser, matchController.confirmRankingSc // 获取正在进行中的比赛 router.get('/ongoing', authUser, matchController.getOngoingMatches); +// 获取选手比赛记录(用于选手详情页) +router.get('/history', authUser, matchController.getPlayerHistory); + // 获取我的比赛记录 router.get('/my-matches', authUser, matchController.getMyMatches);