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 } = 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, { include: [ { model: Store, as: 'store' }, { model: User, as: 'referee' }, { model: MatchPlayer, as: 'players', include: [{ model: LadderUser, as: 'ladderUser', include: [{ model: User, as: 'user' }] }] }, { model: MatchRound, as: 'rounds', include: [{ model: MatchGame, as: 'games' }] }, { model: MatchGame, as: 'games' } ] }); if (!match) { return res.status(404).json(error('比赛不存在', 404)); } 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: match.players.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 } = req.body; // 4, 8, 16 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 rankedPlayers = match.players.sort((a, b) => { if (b.win_count !== a.win_count) return b.win_count - a.win_count; return b.initial_power - a.initial_power; }); const size = Math.min(elimination_size, rankedPlayers.length); const qualifiedPlayers = rankedPlayers.slice(0, size); // 更新比赛状态 await match.update({ stage: RANKING_STAGE.ELIMINATION, elimination_size: size }, { transaction: t }); // 创建淘汰赛首轮(1 vs 最后, 2 vs 倒数第二...) const roundName = 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, ... for (let i = 0; i < size / 2; i++) { 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 }); } await t.commit(); res.json(success(null, '淘汰赛已开始')); } catch (err) { await t.rollback(); console.error('开始淘汰赛失败:', err); res.status(500).json(error('操作失败')); } } // 结束比赛 async endMatch(req, res) { const t = await sequelize.transaction(); try { const { id } = req.params; const match = await Match.findByPk(id, { include: [{ model: MatchPlayer, as: 'players' }] }); if (!match) { await t.rollback(); return res.status(404).json(error('比赛不存在', 404)); } // 计算最终排名和战力值 const players = match.players.sort((a, b) => { if (b.win_count !== a.win_count) return b.win_count - a.win_count; return b.initial_power - a.initial_power; }); for (let i = 0; i < players.length; i++) { const player = players[i]; const ladderUser = await LadderUser.findByPk(player.ladder_user_id); await player.update({ rank: i + 1, final_power: ladderUser.power_score, player_status: 'finished' }, { transaction: t }); // 处理升降级(如果是月度排位赛) if (match.type === MATCH_TYPES.RANKING) { const promotion = PowerCalculator.determinePromotion(i + 1, players.length); if (promotion === 'promote' && ladderUser.level < 5) { await ladderUser.update({ level: ladderUser.level + 1 }, { transaction: t }); } else if (promotion === 'demote' && ladderUser.level > 1) { await ladderUser.update({ level: ladderUser.level - 1 }, { transaction: t }); } } } await match.update({ status: MATCH_STATUS.FINISHED, stage: RANKING_STAGE.FINISHED, end_time: new Date() }, { transaction: t }); 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 { id, gameId } = req.params; const { player1_score, player2_score } = req.body; const game = await MatchGame.findOne({ where: { id: gameId, match_id: 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 winnerId = player1_score > player2_score ? game.player1_id : game.player2_id; const loserId = player1_score > player2_score ? game.player2_id : game.player1_id; await game.update({ player1_score, player2_score, winner_id: winnerId, loser_id: loserId }); res.json(success(null, '更新成功')); } catch (err) { console.error('修改对局结果失败:', err); res.status(500).json(error('修改失败')); } } // 裁判确认对局结果 async confirmGameResult(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' }] }); 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 }); await t.commit(); 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'); if (waitingPlayers.length < 2) return; // 获取未完成的对局 const pendingGames = await MatchGame.findAll({ where: { match_id: matchId, status: 0 }, transaction }); // 找到可以匹配的选手对 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) { // 更新选手状态 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 }); // 从等待列表移除 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; } } } } module.exports = new MatchAdminController();