const { Match, MatchGame, MatchPlayer, MatchRound, LadderUser, User, Store, sequelize } = require('../models'); const { MATCH_TYPES, MATCH_STATUS, RANKING_STAGE, CONFIRM_STATUS } = require('../config/constants'); const { generateMatchCode, success, error, getPagination, pageResult } = require('../utils/helper'); const PowerCalculator = require('../services/powerCalculator'); const { broadcastToUsers, sendMatchNotification } = require('../websocket'); const { Op } = require('sequelize'); class MatchAdminController { // 获取比赛列表 async getList(req, res) { try { const { page = 1, pageSize = 20, type, status, store_id } = req.query; const { limit, offset } = getPagination(page, pageSize); const admin = req.admin; const where = {}; // 门店权限过滤 if (admin.role === 'super_admin') { if (store_id) { where.store_id = store_id; } } else { where.store_id = admin.store_id; } if (type) { where.type = type; } if (status !== undefined && status !== '') { where.status = status; } const { rows, count } = await Match.findAndCountAll({ where, include: [ { model: Store, as: 'store', attributes: ['id', 'name'] }, { model: User, as: 'referee', attributes: ['id', 'nickname'] } ], order: [['created_at', 'DESC']], limit, offset }); res.json(pageResult(rows.map(match => ({ id: match.id, matchCode: match.match_code, type: match.type, name: match.name, weight: match.weight, storeId: match.store_id, storeName: match.store?.name, refereeName: match.referee?.nickname, status: match.status, stage: match.stage, startTime: match.start_time, endTime: match.end_time, createdAt: match.created_at })), count, page, pageSize)); } catch (err) { console.error('获取比赛列表失败:', err); res.status(500).json(error('获取失败')); } } // 创建排位赛 async create(req, res) { try { const { store_id, name, weight, referee_id } = req.body; const admin = req.admin; const targetStoreId = admin.role === 'super_admin' ? store_id : admin.store_id; if (!targetStoreId) { return res.status(400).json(error('请选择门店', 400)); } const match = await Match.create({ store_id: targetStoreId, match_code: generateMatchCode(), type: MATCH_TYPES.RANKING, name: name || `排位赛 ${new Date().toLocaleDateString()}`, weight: weight || 1.5, referee_id, status: MATCH_STATUS.PENDING, stage: RANKING_STAGE.SIGNUP }); res.json(success({ id: match.id, matchCode: match.match_code }, '创建成功')); } catch (err) { console.error('创建比赛失败:', err); res.status(500).json(error('创建失败')); } } // 获取比赛详情 async getDetail(req, res) { try { const { id } = req.params; const match = await Match.findByPk(id, { attributes: [ 'id', 'match_code', 'type', 'name', 'weight', 'status', 'stage', 'elimination_size', 'store_id', 'referee_id', 'start_time', 'end_time' ], include: [ { model: Store, as: 'store', attributes: ['id', 'name'] }, { model: User, as: 'referee', attributes: ['id', 'nickname'] }, { model: MatchPlayer, as: 'players', separate: true, attributes: [ 'id', 'ladder_user_id', 'initial_power', 'final_power', 'win_count', 'lose_count', 'rank', 'player_status' ], include: [ { model: LadderUser, as: 'ladderUser', attributes: ['id', 'real_name', 'level'], include: [ { model: User, as: 'user', attributes: ['id', 'nickname'] } ] } ] }, { model: MatchRound, as: 'rounds', separate: true, attributes: ['id', 'round_number', 'round_type', 'round_name', 'status'], include: [ { model: MatchGame, as: 'games', attributes: [ 'id', 'player1_id', 'player2_id', 'player1_score', 'player2_score', 'winner_id', 'confirm_status', 'status' ] } ] }, { model: MatchGame, as: 'games', separate: true, attributes: [ 'id', 'player1_id', 'player2_id', 'player1_score', 'player2_score', 'winner_id', 'confirm_status', 'status' ] } ] }); if (!match) { return res.status(404).json(error('比赛不存在', 404)); } // 对选手进行排序:如果比赛已结束且有 rank,按 rank 排序;否则按胜场、总得分、初始战力排序 const sortedPlayers = [...match.players]; if (match.status === MATCH_STATUS.FINISHED && sortedPlayers.some(p => p.rank)) { // 比赛已结束:按 rank 排序 sortedPlayers.sort((a, b) => { if (a.rank && b.rank) return a.rank - b.rank; if (a.rank) return -1; if (b.rank) return 1; return 0; }); } else { // 比赛进行中:按胜场、总得分、初始战力排序 // 需要计算总得分 const allGames = match.games || []; const scoreMap = new Map(); allGames.forEach(g => { if (g.confirm_status === CONFIRM_STATUS.CONFIRMED) { const key1 = String(g.player1_id); const key2 = String(g.player2_id); scoreMap.set(key1, (scoreMap.get(key1) || 0) + (g.player1_score || 0)); scoreMap.set(key2, (scoreMap.get(key2) || 0) + (g.player2_score || 0)); } }); sortedPlayers.sort((a, b) => { if (b.win_count !== a.win_count) return b.win_count - a.win_count; const scoreA = scoreMap.get(String(a.ladder_user_id)) || 0; const scoreB = scoreMap.get(String(b.ladder_user_id)) || 0; if (scoreB !== scoreA) return scoreB - scoreA; return b.initial_power - a.initial_power; }); } res.json(success({ id: match.id, matchCode: match.match_code, type: match.type, name: match.name, weight: match.weight, status: match.status, stage: match.stage, eliminationSize: match.elimination_size, storeId: match.store_id, storeName: match.store?.name, refereeId: match.referee_id, refereeName: match.referee?.nickname, startTime: match.start_time, endTime: match.end_time, players: sortedPlayers.map(p => ({ id: p.id, ladderUserId: p.ladder_user_id, realName: p.ladderUser?.real_name, nickname: p.ladderUser?.user?.nickname, level: p.ladderUser?.level, initialPower: p.initial_power, finalPower: p.final_power, winCount: p.win_count, loseCount: p.lose_count, rank: p.rank, status: p.player_status })), rounds: match.rounds.map(r => ({ id: r.id, roundNumber: r.round_number, roundType: r.round_type, roundName: r.round_name, status: r.status, games: r.games.map(g => ({ id: g.id, player1Id: g.player1_id, player2Id: g.player2_id, player1Score: g.player1_score, player2Score: g.player2_score, winnerId: g.winner_id, confirmStatus: g.confirm_status, status: g.status })) })), games: match.games })); } catch (err) { console.error('获取比赛详情失败:', err); res.status(500).json(error('获取失败')); } } // 更新比赛 async update(req, res) { try { const { id } = req.params; const { name, weight, referee_id } = req.body; const match = await Match.findByPk(id); if (!match) { return res.status(404).json(error('比赛不存在', 404)); } const updateData = {}; if (name !== undefined) updateData.name = name; if (referee_id !== undefined) updateData.referee_id = referee_id; if (weight !== undefined) { const normalizedWeight = parseFloat(weight); if (!Number.isFinite(normalizedWeight) || normalizedWeight <= 0) { return res.status(400).json(error('权重参数无效', 400)); } updateData.weight = normalizedWeight; } if (match.type === MATCH_TYPES.CHALLENGE) { updateData.weight = 1.0; } await match.update(updateData); res.json(success(null, '更新成功')); } catch (err) { console.error('更新比赛失败:', err); res.status(500).json(error('更新失败')); } } // 开始排位赛(进入循环赛) startMatch = async (req, res) => { const t = await sequelize.transaction(); try { const { id } = req.params; const match = await Match.findByPk(id, { include: [{ model: MatchPlayer, as: 'players' }], transaction: t }); if (!match || match.type !== MATCH_TYPES.RANKING) { await t.rollback(); return res.status(400).json(error('比赛不存在或类型错误', 400)); } if (match.stage !== RANKING_STAGE.SIGNUP) { await t.rollback(); return res.status(400).json(error('比赛已开始', 400)); } if (match.players.length < 2) { await t.rollback(); return res.status(400).json(error('参赛人数不足', 400)); } // 更新比赛状态 await match.update({ status: MATCH_STATUS.ONGOING, stage: RANKING_STAGE.ROUND_ROBIN, start_time: new Date() }, { transaction: t }); // 创建循环赛轮次和对局 const players = match.players; const n = players.length; // 生成单循环赛配对 const games = []; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { games.push({ player1_id: players[i].ladder_user_id, player2_id: players[j].ladder_user_id }); } } // 随机打乱顺序 for (let i = games.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [games[i], games[j]] = [games[j], games[i]]; } // 创建轮次 const round = await MatchRound.create({ match_id: match.id, round_number: 1, round_type: 'round_robin', round_name: '循环赛', status: 1 }, { transaction: t }); // 创建对局 for (const game of games) { await MatchGame.create({ match_id: match.id, round_id: round.id, player1_id: game.player1_id, player2_id: game.player2_id, status: 0 }, { transaction: t }); } // 自动匹配第一轮 // 注意:这里需要传入 transaction,并且在 _autoMatchPlayers 中使用该 transaction 查询刚创建的数据 await this._autoMatchPlayers(match.id, t); await t.commit(); res.json(success(null, '比赛已开始')); } catch (err) { await t.rollback(); console.error('开始比赛失败 (Stack):', err.stack || err); // 打印完整堆栈 console.error('开始比赛失败 (Msg):', err.message || err); res.status(500).json(error('操作失败: ' + (err.message || '未知错误'))); } } // 开始淘汰赛 async startElimination(req, res) { const t = await sequelize.transaction(); try { const { id } = req.params; const { elimination_size, selected_player_ids } = req.body; // elimination_size: 2/4/8..., selected_player_ids: 可选,自定义选手ID列表(ladder_user_id) const match = await Match.findByPk(id, { include: [{ model: MatchPlayer, as: 'players' }] }); if (!match || match.stage !== RANKING_STAGE.ROUND_ROBIN) { await t.rollback(); return res.status(400).json(error('比赛状态错误', 400)); } // 检查循环赛是否结束 const unfinishedGames = await MatchGame.count({ where: { match_id: id, confirm_status: { [Op.ne]: CONFIRM_STATUS.CONFIRMED } }, include: [{ model: MatchRound, as: 'round', where: { round_type: 'round_robin' } }] }); if (unfinishedGames > 0) { await t.rollback(); return res.status(400).json(error('循环赛尚未结束', 400)); } // 根据胜场和总得分排名(循环赛阶段只统计循环赛已确认对局) const confirmedGames = await MatchGame.findAll({ where: { match_id: id, confirm_status: CONFIRM_STATUS.CONFIRMED }, include: [{ model: MatchRound, as: 'round', where: { round_type: 'round_robin' } }] }); const scoreMap = new Map(); // ladder_user_id -> 总得分(自己得分之和) const addScore = (playerId, score) => { const key = String(playerId); const prev = scoreMap.get(key) || 0; scoreMap.set(key, prev + (Number(score) || 0)); }; confirmedGames.forEach(g => { addScore(g.player1_id, g.player1_score); addScore(g.player2_id, g.player2_score); }); const rankedPlayers = match.players.sort((a, b) => { if (b.win_count !== a.win_count) return b.win_count - a.win_count; const scoreA = scoreMap.get(String(a.ladder_user_id)) || 0; const scoreB = scoreMap.get(String(b.ladder_user_id)) || 0; if (scoreB !== scoreA) return scoreB - scoreA; // 再次平局时保持初始战力作为第三排序因子 return b.initial_power - a.initial_power; }); // 支持手动选择淘汰赛选手: // - 如果传入 selected_player_ids(ladder_user_id 列表),则优先使用该列表 // - 否则按排名取前 elimination_size 名 let size; let qualifiedPlayers; if (Array.isArray(selected_player_ids) && selected_player_ids.length >= 2) { const idSet = new Set(selected_player_ids.map((v) => String(v))); const picked = match.players.filter((p) => idSet.has(String(p.ladder_user_id)), ); if (picked.length < 2) { await t.rollback(); return res.status(400).json(error('所选选手数量不足 2 人', 400)); } size = picked.length; qualifiedPlayers = picked; } else { const safeSize = Math.min(elimination_size || 2, rankedPlayers.length); if (safeSize < 2) { await t.rollback(); return res.status(400).json(error('参赛人数不足以开启淘汰赛', 400)); } size = safeSize; qualifiedPlayers = rankedPlayers.slice(0, size); } // 更新比赛状态 await match.update({ stage: RANKING_STAGE.ELIMINATION, elimination_size: size }, { transaction: t }); // 根据人数确定轮次名称 // 2人:直接决赛 // 4人:半决赛 // 8人:四分之一决赛 // 其他:淘汰赛首轮 const roundName = size === 2 ? '决赛' : size === 4 ? '半决赛' : size === 8 ? '四分之一决赛' : '淘汰赛首轮'; const round = await MatchRound.create({ match_id: match.id, round_number: 100, // 淘汰赛轮次从100开始 round_type: 'elimination', round_name: roundName, status: 1 }, { transaction: t }); // 配对:1 vs size, 2 vs size-1, ... const createdGames = []; for (let i = 0; i < size / 2; i++) { const game = await MatchGame.create({ match_id: match.id, round_id: round.id, player1_id: qualifiedPlayers[i].ladder_user_id, player2_id: qualifiedPlayers[size - 1 - i].ladder_user_id, status: 0 }, { transaction: t }); createdGames.push({ game, player1: qualifiedPlayers[i], player2: qualifiedPlayers[size - 1 - i] }); } // 将晋级淘汰赛的选手状态标记为进行中(playing),方便前端展示 await MatchPlayer.update( { player_status: 'playing' }, { where: { match_id: match.id, ladder_user_id: qualifiedPlayers.map(p => p.ladder_user_id) }, transaction: t } ); await t.commit(); // 发送WebSocket通知给所有匹配的选手 try { for (const { game, player1, player2 } of createdGames) { // 获取选手的天梯用户信息(包含user_id) const [ladder1, ladder2] = await Promise.all([ LadderUser.findByPk(player1.ladder_user_id, { include: [{ model: User, as: 'user', attributes: ['id', 'nickname', 'avatar'] }] }), LadderUser.findByPk(player2.ladder_user_id, { include: [{ model: User, as: 'user', attributes: ['id', 'nickname', 'avatar'] }] }) ]); // 通知选手1 if (ladder1 && ladder1.user_id) { sendMatchNotification(ladder1.user_id, { matchId: match.id, matchCode: match.match_code, opponent: { id: ladder2?.id, realName: ladder2?.real_name, level: ladder2?.level, powerScore: ladder2?.power_score, nickname: ladder2?.user?.nickname, avatar: ladder2?.user?.avatar } }); } // 通知选手2 if (ladder2 && ladder2.user_id) { sendMatchNotification(ladder2.user_id, { matchId: match.id, matchCode: match.match_code, opponent: { id: ladder1?.id, realName: ladder1?.real_name, level: ladder1?.level, powerScore: ladder1?.power_score, nickname: ladder1?.user?.nickname, avatar: ladder1?.user?.avatar } }); } } } catch (notifyErr) { console.error('发送淘汰赛匹配通知失败:', notifyErr); // 通知失败不影响主流程,只记录错误 } res.json(success(null, '淘汰赛已开始')); } catch (err) { await t.rollback(); console.error('开始淘汰赛失败:', err); res.status(500).json(error('操作失败')); } } // 结束比赛(手动触发,一般由后台按钮调用) endMatch = async (req, res) => { const t = await sequelize.transaction(); try { const { id } = req.params; const finished = await this._autoFinishRankingMatch(id, t, true); if (!finished) { await t.rollback(); return res.status(404).json(error('比赛不存在或尚未结束所有对局', 404)); } await t.commit(); res.json(success(null, '比赛已结束')); } catch (err) { await t.rollback(); console.error('结束比赛失败:', err); res.status(500).json(error('操作失败')); } } // 修改对局结果 async updateGameResult(req, res) { try { const { matchId, id, gameId } = req.params; const match_id = matchId ?? id; const { player1_score, player2_score } = req.body; if (!match_id || !gameId) { return res.status(400).json(error('参数缺失', 400)); } const game = await MatchGame.findOne({ where: { id: gameId, match_id } }); if (!game) { return res.status(404).json(error('对局不存在', 404)); } if (game.confirm_status === CONFIRM_STATUS.CONFIRMED) { return res.status(400).json(error('已确认的对局不能修改', 400)); } const p1 = Number(player1_score); const p2 = Number(player2_score); if (!Number.isFinite(p1) || !Number.isFinite(p2)) { return res.status(400).json(error('比分无效', 400)); } if (p1 < 0 || p2 < 0) { return res.status(400).json(error('比分不能为负数', 400)); } if (p1 === p2) { return res.status(400).json(error('比分不能相同', 400)); } const winnerId = p1 > p2 ? game.player1_id : game.player2_id; const loserId = p1 > p2 ? game.player2_id : game.player1_id; await game.update({ player1_score: p1, player2_score: p2, winner_id: winnerId, loser_id: loserId }); res.json(success(null, '更新成功')); } catch (err) { console.error('修改对局结果失败:', err); res.status(500).json(error('修改失败')); } } // 裁判确认对局结果 confirmGameResult = async (req, res) => { const t = await sequelize.transaction(); try { const { id, gameId } = req.params; const game = await MatchGame.findOne({ where: { id: gameId, match_id: id }, include: [{ model: Match, as: 'match' }], transaction: t }); if (!game) { await t.rollback(); return res.status(404).json(error('对局不存在', 404)); } if (!game.winner_id) { await t.rollback(); return res.status(400).json(error('请先填写比分', 400)); } // 计算战力值变动 const winner = await LadderUser.findByPk(game.winner_id); const loser = await LadderUser.findByPk(game.loser_id); const { winnerChange, loserChange } = PowerCalculator.calculate( { powerScore: winner.power_score, level: winner.level, protectedMatches: winner.protected_matches }, { powerScore: loser.power_score, level: loser.level, protectedMatches: loser.protected_matches }, parseFloat(game.match.weight) ); // 更新游戏记录 await game.update({ confirm_status: CONFIRM_STATUS.CONFIRMED, confirmed_by: req.admin.id, confirmed_at: new Date(), winner_power_change: winnerChange, loser_power_change: loserChange, status: 2 }, { transaction: t }); // 更新战力值 await winner.update({ power_score: winner.power_score + winnerChange, match_count: winner.match_count + 1, monthly_match_count: winner.monthly_match_count + 1, win_count: winner.win_count + 1, last_match_time: new Date() }, { transaction: t }); await loser.update({ power_score: Math.max(0, loser.power_score + loserChange), match_count: loser.match_count + 1, monthly_match_count: loser.monthly_match_count + 1, last_match_time: new Date() }, { transaction: t }); // 更新选手统计 await MatchPlayer.increment('win_count', { where: { match_id: id, ladder_user_id: game.winner_id }, transaction: t }); await MatchPlayer.increment('lose_count', { where: { match_id: id, ladder_user_id: game.loser_id }, transaction: t }); // 排位赛:根据阶段做不同处理 if (game.match && game.match.type === MATCH_TYPES.RANKING) { if (game.match.stage === RANKING_STAGE.ROUND_ROBIN) { // 循环赛:确认后双方回到 waiting,并自动匹配下一位空闲对手 await MatchPlayer.update( { player_status: 'waiting', current_opponent_id: null }, { where: { match_id: id, ladder_user_id: [game.winner_id, game.loser_id] }, transaction: t } ); await this._autoMatchPlayers(game.match.id, t); } else if (game.match.stage === RANKING_STAGE.ELIMINATION) { // 淘汰赛:先检查是否需要自动生成下一轮(半决赛 -> 决赛) await this._maybeStartNextEliminationRound(game.match.id, t); } // 排位赛:若所有对局均已确认,自动结算比赛并生成最终排名 await this._autoFinishRankingMatch(game.match.id, t); } await t.commit(); // 排位赛:通知双方刷新当前对局(含新匹配) if (game.match && game.match.type === MATCH_TYPES.RANKING) { const rankingUserIds = [winner.user_id, loser.user_id].filter(Boolean); if (rankingUserIds.length > 0) { broadcastToUsers(rankingUserIds, { type: 'ranking_game_updated', data: { matchId: game.match.id, matchCode: game.match.match_code, gameId: game.id } }); } } res.json(success({ winnerChange, loserChange }, '确认成功')); } catch (err) { await t.rollback(); console.error('确认对局失败:', err); res.status(500).json(error('确认失败')); } } // 手动添加比赛选手 async addPlayer(req, res) { try { const { id } = req.params; const { user_id } = req.body; const match = await Match.findByPk(id); if (!match) { return res.status(404).json(error('比赛不存在', 404)); } if (match.status !== MATCH_STATUS.PENDING) { return res.status(400).json(error('比赛已开始或结束,无法添加选手', 400)); } // 查找该用户在对应门店的选手信息 const ladderUser = await LadderUser.findOne({ where: { user_id, store_id: match.store_id } }); if (!ladderUser) { return res.status(400).json(error('该用户未在本店注册选手信息', 400)); } // 检查是否已参赛 const existingPlayer = await MatchPlayer.findOne({ where: { match_id: id, ladder_user_id: ladderUser.id } }); if (existingPlayer) { return res.status(400).json(error('该选手已在比赛中', 400)); } // 添加选手 await MatchPlayer.create({ match_id: id, ladder_user_id: ladderUser.id, initial_power: ladderUser.power_score, player_status: 'waiting' }); res.json(success(null, '添加成功')); } catch (err) { console.error('添加选手失败:', err); res.status(500).json(error('添加失败')); } } // 移除比赛选手 async removePlayer(req, res) { try { const { id, playerId } = req.params; const match = await Match.findByPk(id); if (!match) { return res.status(404).json(error('比赛不存在', 404)); } if (match.status !== MATCH_STATUS.PENDING) { return res.status(400).json(error('比赛已开始或结束,无法移除选手', 400)); } // 查找选手记录 const matchPlayer = await MatchPlayer.findOne({ where: { match_id: id, id: playerId } }); if (!matchPlayer) { return res.status(404).json(error('该选手未参加此比赛', 404)); } // 移除选手 await matchPlayer.destroy(); res.json(success(null, '移除成功')); } catch (err) { console.error('移除选手失败:', err); res.status(500).json(error('移除失败')); } } // 自动匹配选手 async _autoMatchPlayers(matchId, transaction) { const match = await Match.findByPk(matchId, { include: [{ model: MatchPlayer, as: 'players' }], transaction }); // 获取等待中的选手 const waitingPlayers = match.players.filter(p => p.player_status === 'waiting'); console.log(`[自动匹配] 比赛 ${matchId}: 等待中的选手数量=${waitingPlayers.length}`, waitingPlayers.map(p => ({ id: p.ladder_user_id, status: p.player_status }))); if (waitingPlayers.length < 2) { console.log(`[自动匹配] 等待中的选手不足2人,无法匹配`); return; } // 获取未完成的对局(排除已确认的对局) // 注意:status=0 表示待比赛,status=1 表示比赛中,status=2 表示已结束 // 只有 status=0 且未确认的对局才能被匹配 let pendingGames = await MatchGame.findAll({ where: { match_id: matchId, status: 0, confirm_status: { [Op.ne]: CONFIRM_STATUS.CONFIRMED } }, transaction }); console.log(`[自动匹配] 比赛 ${matchId}: 待匹配的对局数量=${pendingGames.length}`, pendingGames.map(g => ({ id: g.id, p1: g.player1_id, p2: g.player2_id, status: g.status, confirm_status: g.confirm_status }))); // 如果待匹配的对局数量为0,检查是否有状态异常的对局(status=1但未确认,可能是之前匹配过但确认失败) // 如果两个选手都是 waiting 状态,说明对局应该被重置,允许重新匹配 if (pendingGames.length === 0) { const abnormalGames = await MatchGame.findAll({ where: { match_id: matchId, status: 1, // 只检查 status=1 的对局(status=2 且未确认的可能是其他情况) confirm_status: { [Op.ne]: CONFIRM_STATUS.CONFIRMED } }, transaction }); if (abnormalGames.length > 0) { console.log(`[自动匹配] 发现 ${abnormalGames.length} 个状态异常的对局(已开始但未确认)`, abnormalGames.map(g => ({ id: g.id, p1: g.player1_id, p2: g.player2_id, status: g.status, confirm_status: g.confirm_status }))); // 检查这些异常对局的两个选手是否都是 waiting 状态 for (const game of abnormalGames) { const p1 = waitingPlayers.find(p => String(p.ladder_user_id) === String(game.player1_id)); const p2 = waitingPlayers.find(p => String(p.ladder_user_id) === String(game.player2_id)); if (p1 && p2) { // 两个选手都是 waiting,说明对局应该被重置 console.log(`[自动匹配] 重置异常对局 ${game.id}: 选手 ${game.player1_id} 和 ${game.player2_id} 都是 waiting 状态`); await game.update({ status: 0, // 重置为待比赛状态 player1_score: null, player2_score: null, winner_id: null, loser_id: null, submit_by: null }, { transaction }); // 将这个对局添加到待匹配列表 pendingGames.push(game); } } } } // 找到可以匹配的选手对 for (const game of pendingGames) { // 使用 == 比较,避免 string/number 类型不一致问题 // 注意:player1_id 和 player2_id 是 bigint,可能是 string 类型 // ladder_user_id 也是 bigint,可能是 string 类型 const player1 = waitingPlayers.find(p => String(p.ladder_user_id) === String(game.player1_id)); const player2 = waitingPlayers.find(p => String(p.ladder_user_id) === String(game.player2_id)); if (player1 && player2) { console.log(`[自动匹配] 匹配成功: 对局 ${game.id}, 选手1=${player1.ladder_user_id}, 选手2=${player2.ladder_user_id}`); // 更新选手状态 await MatchPlayer.update( { player_status: 'playing', current_opponent_id: game.player2_id }, { where: { id: player1.id }, transaction } ); await MatchPlayer.update( { player_status: 'playing', current_opponent_id: game.player1_id }, { where: { id: player2.id }, transaction } ); // 更新对局状态 await game.update({ status: 1 }, { transaction }); // 发送匹配成功 WebSocket 通知给双方 try { const [ladder1, ladder2] = await Promise.all([ LadderUser.findByPk(game.player1_id, { include: [{ model: User, as: 'user', attributes: ['id', 'nickname', 'avatar'] }], transaction }), LadderUser.findByPk(game.player2_id, { include: [{ model: User, as: 'user', attributes: ['id', 'nickname', 'avatar'] }], transaction }) ]); if (ladder1 && ladder1.user_id) { sendMatchNotification(ladder1.user_id, { matchId: match.id, matchCode: match.match_code, opponent: { id: ladder2?.id, realName: ladder2?.real_name, level: ladder2?.level, powerScore: ladder2?.power_score, nickname: ladder2?.user?.nickname, avatar: ladder2?.user?.avatar } }); } if (ladder2 && ladder2.user_id) { sendMatchNotification(ladder2.user_id, { matchId: match.id, matchCode: match.match_code, opponent: { id: ladder1?.id, realName: ladder1?.real_name, level: ladder1?.level, powerScore: ladder1?.power_score, nickname: ladder1?.user?.nickname, avatar: ladder1?.user?.avatar } }); } } catch (notifyErr) { console.error('发送排位赛匹配通知失败:', notifyErr); } // 从等待列表移除 const idx1 = waitingPlayers.indexOf(player1); const idx2 = waitingPlayers.indexOf(player2); waitingPlayers.splice(Math.max(idx1, idx2), 1); waitingPlayers.splice(Math.min(idx1, idx2), 1); if (waitingPlayers.length < 2) break; } else { console.log(`[自动匹配] 对局 ${game.id} 无法匹配: player1=${player1 ? player1.ladder_user_id : 'null'}, player2=${player2 ? player2.ladder_user_id : 'null'}`); } } console.log(`[自动匹配] 比赛 ${matchId}: 匹配完成,剩余等待选手=${waitingPlayers.length}`); } // 手动触发匹配(用于调试) async triggerMatch(req, res) { const t = await sequelize.transaction(); try { const { id } = req.params; const match = await Match.findByPk(id, { include: [{ model: MatchPlayer, as: 'players' }], transaction: t }); if (!match || match.type !== MATCH_TYPES.RANKING) { await t.rollback(); return res.status(400).json(error('比赛不存在或类型错误', 400)); } if (match.stage !== RANKING_STAGE.ROUND_ROBIN) { await t.rollback(); return res.status(400).json(error('只有循环赛阶段才能手动触发匹配', 400)); } await this._autoMatchPlayers(match.id, t); await t.commit(); res.json(success(null, '匹配已触发')); } catch (err) { await t.rollback(); console.error('手动触发匹配失败:', err); res.status(500).json(error('操作失败')); } } /** * 当淘汰赛某轮全部结束时,自动生成下一轮。 * 当前仅支持:4 强(半决赛)全部确认后自动生成决赛。 */ async _maybeStartNextEliminationRound(matchId, transaction) { const match = await Match.findByPk(matchId, { include: [{ model: MatchRound, as: 'rounds', include: [{ model: MatchGame, as: 'games' }] }], transaction }); if (!match || match.stage !== RANKING_STAGE.ELIMINATION || match.elimination_size !== 4) { return; } // 如果已经有决赛轮次,则不再生成 const hasFinal = match.rounds.some(r => r.round_type === 'elimination' && r.round_name === '决赛'); if (hasFinal) return; // 查找半决赛轮次 const semiRound = match.rounds.find(r => r.round_type === 'elimination' && r.round_name === '半决赛'); if (!semiRound || !semiRound.games || semiRound.games.length < 2) return; // 必须两场都已确认 const allConfirmed = semiRound.games.every(g => g.confirm_status === CONFIRM_STATUS.CONFIRMED); if (!allConfirmed) return; const winnerIds = semiRound.games.map(g => g.winner_id).filter(Boolean); if (winnerIds.length < 2) return; // 创建决赛轮次 const finalRound = await MatchRound.create({ match_id: match.id, round_number: semiRound.round_number + 1, round_type: 'elimination', round_name: '决赛', status: 1 }, { transaction }); // 创建决赛对局(两位半决赛胜者对决) await MatchGame.create({ match_id: match.id, round_id: finalRound.id, player1_id: winnerIds[0], player2_id: winnerIds[1], status: 0 }, { transaction }); // 将两名决赛选手标记为进行中 await MatchPlayer.update( { player_status: 'playing', current_opponent_id: null }, { where: { match_id: match.id, ladder_user_id: winnerIds }, transaction } ); } /** * 在所有对局都确认后,自动结算排位赛,生成最终排名。 * 如果 force=true,则只检查比赛是否存在,不再检查“是否还有未确认对局”(用于手动结束按钮)。 */ async _autoFinishRankingMatch(matchId, transaction, force = false) { const match = await Match.findByPk(matchId, { include: [ { model: MatchPlayer, as: 'players' }, { model: MatchRound, as: 'rounds', include: [{ model: MatchGame, as: 'games' }] } ], transaction }); if (!match) return false; // 如果不是排位赛,暂不处理 if (match.type !== MATCH_TYPES.RANKING) return false; // 如果还在循环赛阶段,不自动结束(需要手动开始淘汰赛) if (match.stage === RANKING_STAGE.ROUND_ROBIN) { return false; } // 如果不在淘汰赛阶段,也不自动结束(可能是其他状态) if (match.stage !== RANKING_STAGE.ELIMINATION) { return false; } if (!force) { // 检查是否还有未确认的对局,有的话暂不结束 const remainingGames = await MatchGame.count({ where: { match_id: matchId, confirm_status: { [Op.ne]: CONFIRM_STATUS.CONFIRMED } }, transaction }); if (remainingGames > 0) { return false; } // 检查是否有决赛,并且决赛是否已确认 const finalRound = match.rounds.find(r => r.round_type === 'elimination' && r.round_name === '决赛'); if (!finalRound || !finalRound.games || finalRound.games.length === 0) { // 没有决赛,不自动结束 return false; } // 检查决赛是否已确认 const finalGame = finalRound.games[0]; if (finalGame.confirm_status !== CONFIRM_STATUS.CONFIRMED) { // 决赛未确认,不自动结束 return false; } } // 计算最终排名:先按胜场,再按总得分(自己得分总和),最后按初始战力 const confirmedGamesAll = await MatchGame.findAll({ where: { match_id: matchId, confirm_status: CONFIRM_STATUS.CONFIRMED }, transaction }); const scoreMap = new Map(); // ladder_user_id -> 总得分 const addScore = (playerId, score) => { const key = String(playerId); const prev = scoreMap.get(key) || 0; scoreMap.set(key, prev + (Number(score) || 0)); }; confirmedGamesAll.forEach(g => { addScore(g.player1_id, g.player1_score); addScore(g.player2_id, g.player2_score); }); // 先创建一个副本进行排序,避免修改原数组 const players = [...match.players].sort((a, b) => { // 第一优先级:胜场数(降序) if (b.win_count !== a.win_count) { return b.win_count - a.win_count; } // 第二优先级:总得分(降序) const scoreA = scoreMap.get(String(a.ladder_user_id)) || 0; const scoreB = scoreMap.get(String(b.ladder_user_id)) || 0; if (scoreB !== scoreA) { return scoreB - scoreA; } // 第三优先级:初始战力(降序) return b.initial_power - a.initial_power; }); // 调试日志:打印排序结果 console.log(`[最终排名] 比赛 ${matchId} 排序结果:`, players.map((p, idx) => ({ rank: idx + 1, name: p.ladderUser?.real_name || p.ladder_user_id, win_count: p.win_count, total_score: scoreMap.get(String(p.ladder_user_id)) || 0, initial_power: p.initial_power }))); for (let i = 0; i < players.length; i++) { const player = players[i]; const ladderUser = await LadderUser.findByPk(player.ladder_user_id, { transaction }); if (!ladderUser) continue; await player.update( { rank: i + 1, final_power: ladderUser.power_score, player_status: 'finished', }, { transaction } ); // 处理升降级(排位赛规则) const promotion = PowerCalculator.determinePromotion(i + 1, players.length); if (promotion === 'promote' && ladderUser.level < 5) { // 冠军:段位 +1 await ladderUser.update({ level: ladderUser.level + 1 }, { transaction }); } else if (promotion === 'demote' && (ladderUser.level === 4 || ladderUser.level === 5)) { // 末位:只有 4、5 段才降一级 await ladderUser.update({ level: ladderUser.level - 1 }, { transaction }); } // 其他情况不变 } await match.update( { status: MATCH_STATUS.FINISHED, stage: RANKING_STAGE.FINISHED, end_time: new Date(), }, { transaction } ); return true; } } module.exports = new MatchAdminController();