yingsa/server/src/controllers/matchAdminController.js
ethanfly 8f9eb38666 fix(dependencies): Remove peer flags from package-lock.json and update Vite dependencies
- Removed unnecessary "peer" flags from various dependencies in package-lock.json to streamline package management.
- Updated Vite dependencies and their corresponding metadata for improved performance and compatibility.
- Adjusted import paths in CSS files to reflect the correct directory structure.
- Deleted unused CSS files related to the "col" component to clean up the project.
2026-02-07 02:05:34 +08:00

1286 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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_idsladder_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();