Migrate project from typing_practiceweb

This commit is contained in:
2026-05-14 16:04:14 +08:00
parent dbc5425706
commit 0e87d5546d
225 changed files with 92811 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
// server/websocket/minesweeperSocket.ts
import { Server as SocketIOServer } from 'socket.io';
import { Server as HTTPServer } from 'http';
import dotenv from 'dotenv';
// 加载环境变量
dotenv.config();
interface Cell {
isMine: boolean;
isRevealed: boolean;
isFlagged: boolean;
neighborMines: number;
isExploded?: boolean;
}
interface GameRoom {
roomId: string;
playerId: string;
board: Cell[][];
difficulty: string;
spectators: Set<string>;
highlightedCells: Set<string>; // 存储需要闪烁的格子坐标 "row,col"
invitePlayMode: boolean; // 是否邀请同玩模式
}
// 存储所有游戏房间
const gameRooms = new Map<string, GameRoom>();
// 获取 WebSocket 路径前缀 - 与客户端保持一致
function getSocketIoPath() {
const envApiUrl = process.env.REACT_APP_API_BASE_URL;
console.log('[Socket.IO Path Config] REACT_APP_API_BASE_URL:', envApiUrl);
console.log('[Socket.IO Path Config] NODE_ENV:', process.env.NODE_ENV);
if (!envApiUrl || envApiUrl.trim() === '') {
// 如果没有设置环境变量,根据 NODE_ENV 决定
const path = process.env.NODE_ENV === 'production' ? '/api/socket.io' : '/socket.io';
console.log('[Socket.IO Path Config] Using default path:', path);
return path;
}
// 根据Nginx转发规则调整生产环境路径
// Nginx转发到 http://localhost:5001/api/socket.io/,所以后端应该接受 /api/socket.io
if (process.env.NODE_ENV === 'production') {
console.log('[Socket.IO Path Config] Production mode, Nginx配置');
console.log('[Socket.IO Path Config] Frontend sends: /api/api/socket.io');
console.log('[Socket.IO Path Config] Nginx location = /api/api/socket.io/');
console.log('[Socket.IO Path Config] Nginx proxy_pass: http://localhost:5001/api/socket.io/');
console.log('[Socket.IO Path Config] Backend should accept: /api/socket.io');
// 使用 /api/socket.io 路径匹配Nginx转发结果
return '/api/socket.io';
}
// 如果是完整 URL检查是否包含路径
if (envApiUrl.startsWith('http://') || envApiUrl.startsWith('https://')) {
const urlObj = new URL(envApiUrl);
// 如果原始 URL 包含路径,将其作为前缀
if (urlObj.pathname && urlObj.pathname !== '/') {
const path = `${urlObj.pathname}/socket.io`;
console.log('[Socket.IO Path Config] From URL pathname:', path);
return path;
}
}
// 如果是相对路径(如 /api将其作为前缀
if (envApiUrl.startsWith('/')) {
const path = `${envApiUrl}/socket.io`;
console.log('[Socket.IO Path Config] From relative path:', path);
return path;
}
const path = '/socket.io';
console.log('[Socket.IO Path Config] Using fallback path:', path);
return path;
}
export function setupMinesweeperSocket(httpServer: HTTPServer) {
// 生产环境支持多种可能的路径,通过请求检测来确定
const possiblePaths = ['/api/socket.io', '/socket.io'];
const socketIoPath = getSocketIoPath();
console.log('[Socket.IO] Socket.IO 路径配置:', socketIoPath);
console.log('[Socket.IO] 支持的可能路径:', possiblePaths);
console.log('[Socket.IO] HTTP Server listening on port:', httpServer.address());
const io = new SocketIOServer(httpServer, {
path: socketIoPath, // 使用最常见的路径作为配置
cors: {
origin: "*",
methods: ["GET", "POST"]
},
allowEIO3: true,
transports: ['polling', 'websocket']
});
// 添加底层HTTP请求日志中间件
io.engine.on('initial_headers', (headers, req) => {
console.log('[Socket.IO Engine] === INITIAL HEADERS ===');
console.log('[Socket.IO Engine] Method:', req.method);
console.log('[Socket.IO Engine] URL:', req.url);
console.log('[Socket.IO Engine] Headers:', JSON.stringify(req.headers, null, 2));
console.log('[Socket.IO Engine] Query:', req.url ? req.url.split('?')[1] : 'N/A');
console.log('[Socket.IO Engine] ========================');
});
io.engine.on('headers', (headers, req) => {
console.log('[Socket.IO Engine] === RESPONSE HEADERS ===');
console.log('[Socket.IO Engine] Status:', headers.status || 200);
console.log('[Socket.IO Engine] Headers:', JSON.stringify(headers, null, 2));
console.log('[Socket.IO Engine] ========================');
});
io.on('connection', (socket) => {
console.log('[Socket.IO] >>> CONNECTION ESTABLISHED <<<');
console.log('[Socket.IO] 用户连接:', socket.id);
console.log('[Socket.IO] 握手耗时:', Date.now() - (socket.handshake.time || Date.now()), 'ms');
console.log('[Socket.IO] 握手 URL:', socket.handshake.url);
console.log('[Socket.IO] 握手查询参数:', socket.handshake.query);
console.log('[Socket.IO] 握手 Origin:', socket.handshake.headers.origin);
console.log('[Socket.IO] 握手 Referer:', socket.handshake.headers.referer);
console.log('[Socket.IO] 握手 User-Agent:', socket.handshake.headers['user-agent']);
console.log('[Socket.IO] 握手 Host:', socket.handshake.headers.host);
console.log('[Socket.IO] 配置的 Socket.IO 路径:', socketIoPath);
// 详细路径分析 - 规范化路径比较(处理末尾斜杠差异)
const reqPath = socket.handshake.url ? socket.handshake.url.split('?')[0] : 'N/A';
const normalizedReqPath = reqPath.replace(/\/$/, ''); // 移除末尾斜杠
const normalizedConfigPath = socketIoPath.replace(/\/$/, ''); // 移除末尾斜杠
console.log('[Socket.IO] 路径分析:');
console.log(' - 原始请求路径:', reqPath);
console.log(' - 规范化请求路径:', normalizedReqPath);
console.log(' - 配置路径:', socketIoPath);
console.log(' - 规范化配置路径:', normalizedConfigPath);
console.log(' - 严格相等:', reqPath === socketIoPath);
console.log(' - 规范化后相等:', normalizedReqPath === normalizedConfigPath);
console.log(' - 请求路径.endsWith(配置路径):', reqPath.endsWith(socketIoPath));
console.log(' - 规范化请求.endsWith(规范化配置):', normalizedReqPath.endsWith(normalizedConfigPath));
// 使用规范化路径进行比较
if (normalizedReqPath !== normalizedConfigPath) {
console.warn(`[Socket.IO] ⚠️ 警告: 请求路径与配置路径不匹配!`);
console.warn(`[Socket.IO] 原始请求: ${reqPath}`);
console.warn(`[Socket.IO] 规范化请求: ${normalizedReqPath}`);
console.warn(`[Socket.IO] 配置路径: ${socketIoPath}`);
console.warn(`[Socket.IO] 规范化配置: ${normalizedConfigPath}`);
console.warn(`[Socket.IO] 差异: ${reqPath.replace(socketIoPath, '【配置路径】')}`);
}
// 创建游戏房间(玩家创建)
socket.on('create-room', (data: { difficulty: string; roomId?: string; invitePlayMode?: boolean }) => {
const roomId = data.roomId || generateRoomId();
const room: GameRoom = {
roomId,
playerId: socket.id,
board: [],
difficulty: data.difficulty,
spectators: new Set(),
highlightedCells: new Set(),
invitePlayMode: data.invitePlayMode || false
};
gameRooms.set(roomId, room);
socket.join(roomId);
console.log(`[Socket.IO] 房间创建: ${roomId}, 玩家: ${socket.id}, 邀请同玩模式: ${room.invitePlayMode}`);
socket.emit('room-created', { roomId });
});
// 旁观者加入房间
socket.on('join-spectate', (data: { roomId: string }) => {
const room = gameRooms.get(data.roomId);
if (!room) {
socket.emit('error', { message: '房间不存在' });
return;
}
room.spectators.add(socket.id);
socket.join(data.roomId);
console.log(`旁观者加入: ${socket.id} -> 房间: ${data.roomId}`);
// 发送当前游戏状态给旁观者
socket.emit('game-state', {
board: room.board,
difficulty: room.difficulty
});
});
// 获取房间信息
socket.on('get-room-info', (data: { roomId: string }) => {
console.log('[服务器日志] === 获取房间信息开始 ===');
console.log('[服务器日志] 收到 get-room-info 事件:');
console.log('[服务器日志] - 房间ID:', data.roomId);
console.log('[服务器日志] - 请求者ID:', socket.id);
const room = gameRooms.get(data.roomId);
if (!room) {
console.log('[服务器日志] ❌ 房间不存在');
socket.emit('error', { message: '房间不存在' });
console.log('[服务器日志] === 获取房间信息结束 ===');
return;
}
// 构建房间信息
const roomInfo = {
roomId: room.roomId,
playerCount: 1, // 玩家数量固定为1
spectatorCount: room.spectators.size,
difficulty: room.difficulty,
invitePlayMode: room.invitePlayMode,
gameState: 'playing' // 默认游戏状态为进行中
};
console.log('[服务器日志] ✅ 发送房间信息:');
console.log('[服务器日志] - 同玩模式:', roomInfo.invitePlayMode ? '开启' : '关闭');
console.log('[服务器日志] - 旁观者数量:', roomInfo.spectatorCount);
socket.emit('room-info', roomInfo);
console.log('[服务器日志] === 获取房间信息完成 ===');
});
// 玩家更新游戏状态
socket.on('update-game', (data: { roomId: string; board: Cell[][] }) => {
const room = gameRooms.get(data.roomId);
if (!room || room.playerId !== socket.id) {
return;
}
room.board = data.board;
// 广播给该房间的所有旁观者
socket.to(data.roomId).emit('game-state', {
board: room.board,
difficulty: room.difficulty,
highlightedCells: Array.from(room.highlightedCells)
});
// 清除闪烁的格子
if (room.highlightedCells.size > 0) {
setTimeout(() => {
room.highlightedCells.clear();
}, 100);
}
});
// 旁观者清除所有高亮
socket.on('clear-all-highlights', (data: { roomId: string }) => {
const room = gameRooms.get(data.roomId);
if (!room || !room.spectators.has(socket.id)) {
return;
}
// 清除所有高亮格子
room.highlightedCells.clear();
// 通知玩家清除所有高亮
io.to(room.playerId).emit('clear-all-highlights');
// 通知所有旁观者清除所有高亮
io.to(data.roomId).emit('clear-all-highlights');
console.log(`旁观者清除所有高亮: 房间 ${data.roomId}`);
});
// 旁观者点击格子
socket.on('spectator-click', (data: { roomId: string; row: number; col: number }) => {
const room = gameRooms.get(data.roomId);
if (!room || !room.spectators.has(socket.id)) {
return;
}
const cellKey = `${data.row},${data.col}`;
room.highlightedCells.add(cellKey);
console.log(`旁观者点击: 房间 ${data.roomId}, 格子 (${data.row}, ${data.col})`);
// 通知玩家该格子被旁观者点击
io.to(room.playerId).emit('spectator-suggest', {
row: data.row,
col: data.col
});
// 3秒后移除闪烁
setTimeout(() => {
room.highlightedCells.delete(cellKey);
io.to(data.roomId).emit('clear-highlight', { row: data.row, col: data.col });
}, 3000);
});
// 旁观者操作格子(同玩模式)
socket.on('spectator-action', (data: { roomId: string; action: 'reveal' | 'flag' | 'chord'; row: number; col: number }) => {
console.log('[服务器日志] === 旁观者操作开始 ===');
console.log('[服务器日志] 收到 spectator-action 事件:');
console.log('[服务器日志] - 房间ID:', data.roomId);
console.log('[服务器日志] - 动作类型:', data.action);
console.log('[服务器日志] - 格子坐标:', `(${data.row}, ${data.col})`);
console.log('[服务器日志] - 旁观者ID:', socket.id);
const room = gameRooms.get(data.roomId);
if (!room) {
console.log('[服务器日志] ❌ 房间不存在,操作失败');
console.log('[服务器日志] 当前存在的房间:', Array.from(gameRooms.keys()));
return;
}
console.log('[服务器日志] 房间信息:');
console.log('[服务器日志] - 玩家ID:', room.playerId);
console.log('[服务器日志] - 旁观者数量:', room.spectators.size);
console.log('[服务器日志] - 同玩模式:', room.invitePlayMode);
console.log('[服务器日志] - 当前旁观者:', Array.from(room.spectators));
if (!room.spectators.has(socket.id)) {
console.log('[服务器日志] ❌ 旁观者不在房间中,操作失败');
return;
}
if (!room.invitePlayMode) {
console.log('[服务器日志] ❌ 房间未开启同玩模式,操作失败');
return;
}
console.log(`[服务器日志] ✅ 旁观者操作有效: 房间 ${data.roomId}, 动作: ${data.action}, 格子 (${data.row}, ${data.col})`);
// 转发操作给玩家
console.log('[服务器日志] 转发操作给玩家:', room.playerId);
io.to(room.playerId).emit('spectator-action', {
action: data.action,
row: data.row,
col: data.col
});
console.log('[服务器日志] === 旁观者操作完成 ===');
});
// 玩家更新难度
socket.on('update-difficulty', (data: { roomId: string; difficulty: string; config?: any }) => {
const room = gameRooms.get(data.roomId);
if (!room || room.playerId !== socket.id) {
return;
}
console.log(`玩家更新难度: 房间 ${data.roomId}, 从 ${room.difficulty} 变更为 ${data.difficulty}`);
console.log('配置信息:', data.config);
room.difficulty = data.difficulty;
// 广播难度更新给房间内所有旁观者
io.to(data.roomId).emit('difficulty-updated', {
difficulty: data.difficulty,
config: data.config // 传递配置信息给旁观者
});
});
// 玩家切换同玩模式
socket.on('toggle-invite-play-mode', (data: { roomId: string; invitePlayMode: boolean }) => {
console.log('[服务器日志] === 切换同玩模式开始 ===');
console.log('[服务器日志] 收到 toggle-invite-play-mode 事件:');
console.log('[服务器日志] - 房间ID:', data.roomId);
console.log('[服务器日志] - 同玩模式:', data.invitePlayMode ? '开启' : '关闭');
console.log('[服务器日志] - 玩家ID:', socket.id);
const room = gameRooms.get(data.roomId);
if (!room || room.playerId !== socket.id) {
console.log('[服务器日志] ❌ 房间不存在或玩家不匹配,操作失败');
console.log('[服务器日志] === 切换同玩模式结束 ===');
return;
}
// 更新房间的同玩模式状态
room.invitePlayMode = data.invitePlayMode;
console.log(`[服务器日志] ✅ 同玩模式已更新: ${data.invitePlayMode ? '开启' : '关闭'}`);
// 广播同玩模式更新给房间内所有旁观者
console.log('[服务器日志] 广播同玩模式更新给旁观者');
io.to(data.roomId).emit('invite-play-mode-updated', {
invitePlayMode: data.invitePlayMode
});
console.log('[服务器日志] === 切换同玩模式完成 ===');
});
// 断开连接
socket.on('disconnect', () => {
console.log('用户断开连接:', socket.id);
// 清理房间数据
gameRooms.forEach((room, roomId) => {
if (room.playerId === socket.id) {
// 玩家离开,关闭房间
io.to(roomId).emit('room-closed');
gameRooms.delete(roomId);
console.log(`房间关闭: ${roomId}`);
} else if (room.spectators.has(socket.id)) {
// 旁观者离开
room.spectators.delete(socket.id);
}
});
});
});
return io;
}
// 生成唯一房间ID
function generateRoomId(): string {
return Math.random().toString(36).substring(2, 10).toUpperCase();
}