提交 54b3cf18 authored 作者: lidongxu's avatar lidongxu

增加队伍是否满员检测

上级 03a31ec1
import { io } from 'socket.io-client' /**
* Admin 端 WebSocket 客户端封装
*
* 协议约定(与服务端对应):
* 发送 → JSON.stringify({ event: string, data: any })
* 接收 ← JSON.stringify({ event: string, data: any })
*
* 用法:
* import { connectSocket, getSocket } from './socket';
* connectSocket(); // 在应用初始化时调用
*/
let socket = null // ─── 配置 ──────────────────────────────────────────────────────────────────
/**
* 线上部署说明:
* - nginx: https://paopao.wxl66.cn/api/* → 转发到 http://localhost:3000/*
* 注意:nginx 保留 /api 前缀
* - 后端 WebSocket 路径: /ws
*
* 所以 WebSocket 连接地址为: wss://paopao.wxl66.cn/api/ws
*/
export function connectSocket(baseURL = '') { // 环境切换:true=本地开发,false=线上生产
if (socket?.connected) return socket const isDev = import.meta.env.DEV || window.location.hostname === 'localhost'
const url = baseURL || import.meta.env.VITE_SOCKET_URL || (import.meta.env.DEV ? window.location.origin : '')
socket = io(url, { // WebSocket URL
path: '/socket.io', const WS_URL = isDev
transports: ['websocket', 'polling'], ? 'ws://localhost:3000/ws' // 开发环境
autoConnect: true, : 'wss://paopao.wxl66.cn/api/ws'; // 生产环境:/api(nginx入口) + /ws(后端路由)
})
return socket const RECONNECT_DELAY = 3000
const MAX_RECONNECT_COUNT = 5
// ─── Socket 实例 ───────────────────────────────────────────────────────────
class AdminSocket {
constructor() {
this._ws = null
this._connected = false
this._connecting = false
this._url = WS_URL
this._reconnectCount = 0
this._reconnectTimer = null
this._queue = []
this._listeners = {}
}
/**
* 连接到服务器
* @param {string} [url] 可选,自定义 WebSocket 地址
*/
connect(url) {
if (url) this._url = url
if (this._connected || this._connecting) return
this._doConnect()
}
/**
* 发送自定义事件
* @param {string} event 事件名
* @param {*} data 携带的数据
*/
emit(event, data) {
const raw = JSON.stringify({ event, data })
if (!this._connected) {
this._queue.push(raw)
return
}
this._sendRaw(raw)
}
/**
* 监听服务端推送的事件
* @param {string} event 事件名
* @param {Function} handler 回调
*/
on(event, handler) {
if (!this._listeners[event]) this._listeners[event] = []
this._listeners[event].push(handler)
}
/**
* 移除事件监听
* @param {string} event
* @param {Function} [handler] 不传则移除该事件所有监听
*/
off(event, handler) {
if (!this._listeners[event]) return
if (!handler) {
delete this._listeners[event]
} else {
this._listeners[event] = this._listeners[event].filter(fn => fn !== handler)
}
}
/**
* 主动断开连接
*/
disconnect() {
this._stopReconnect()
this._reconnectCount = MAX_RECONNECT_COUNT
if (this._ws) {
try { this._ws.close() } catch (_) {}
}
this._ws = null
this._connected = false
this._connecting = false
}
/**
* 获取连接状态
*/
get connected() {
return this._connected
}
// ─── 内部实现 ─────────────────────────────────────────────────────────────
_doConnect() {
this._connecting = true
const ws = new WebSocket(this._url)
ws.addEventListener('open', () => {
this._ws = ws
this._connected = true
this._connecting = false
this._reconnectCount = 0
console.log('[AdminSocket] 已连接')
// 发送积压的消息
while (this._queue.length > 0) {
this._sendRaw(this._queue.shift())
}
})
ws.addEventListener('message', (evt) => {
try {
const msg = JSON.parse(evt.data)
const handlers = this._listeners[msg.event]
if (handlers) {
handlers.forEach(fn => {
try { fn(msg.data) } catch (_) {}
})
}
} catch (_) {}
})
ws.addEventListener('close', () => {
this._onDisconnected()
})
ws.addEventListener('error', (err) => {
console.error('[AdminSocket] 连接错误:', err)
this._onDisconnected()
})
}
_onDisconnected() {
this._connected = false
this._connecting = false
this._ws = null
this._scheduleReconnect()
}
_scheduleReconnect() {
if (this._reconnectCount >= MAX_RECONNECT_COUNT) return
this._stopReconnect()
this._reconnectTimer = setTimeout(() => {
this._reconnectCount++
console.log(`[AdminSocket] 第 ${this._reconnectCount} 次重连...`)
this._doConnect()
}, RECONNECT_DELAY)
}
_stopReconnect() {
if (this._reconnectTimer) {
clearTimeout(this._reconnectTimer)
this._reconnectTimer = null
}
}
_sendRaw(raw) {
if (!this._connected || !this._ws) return
try {
this._ws.send(raw)
} catch (_) {}
}
}
// 单例实例
const adminSocket = new AdminSocket()
// ─── 导出兼容接口 ──────────────────────────────────────────────────────────
export function connectSocket(url) {
adminSocket.connect(url)
return adminSocket
} }
export function getSocket() { export function getSocket() {
return socket return adminSocket
} }
export function disconnectSocket() { export function disconnectSocket() {
if (socket) { adminSocket.disconnect()
socket.disconnect()
socket = null
}
} }
export function onRoomState(cb) { export function onRoomState(cb) {
if (!socket) return adminSocket.on('room:state', cb)
socket.on('room:state', cb)
} }
export function offRoomState(cb) { export function offRoomState(cb) {
if (!socket) return adminSocket.off('room:state', cb)
socket.off('room:state', cb)
} }
export function onScreenRoomChanged(cb) { export function onScreenRoomChanged(cb) {
if (!socket) return adminSocket.on('screen:roomChanged', cb)
socket.on('screen:roomChanged', cb)
} }
export function offScreenRoomChanged(cb) { export function offScreenRoomChanged(cb) {
if (!socket) return adminSocket.off('screen:roomChanged', cb)
socket.off('screen:roomChanged', cb)
} }
export default adminSocket
...@@ -7,7 +7,24 @@ ...@@ -7,7 +7,24 @@
*/ */
import { setCurrentRoom, setGameState, clearGameState, setPlayerGameOver, setAllGameOver, setPlayerTeam } from './stateManager.js' import { setCurrentRoom, setGameState, clearGameState, setPlayerGameOver, setAllGameOver, setPlayerTeam } from './stateManager.js'
const defaultServerUrl = import.meta.env.VITE_SOCKET_URL || 'ws://localhost:3000/ws' /**
* WebSocket 服务器地址配置
*
* 部署说明:
* - 本地开发:ws://localhost:3000/ws
* - 线上生产:wss://paopao.wxl66.cn/api/ws
* nginx 会把 /api/ws 转发到 localhost:3000/ws
*
* 环境变量优先级:
* 1. import.meta.env.VITE_SOCKET_URL - 通过 .env 文件配置
* 2. 默认根据当前环境自动选择
*/
const isDev = import.meta.env.DEV || window.location.hostname === 'localhost'
const defaultServerUrl = import.meta.env.VITE_SOCKET_URL || (
isDev
? 'ws://localhost:3000/ws' // 开发环境
: 'wss://paopao.wxl66.cn/api/ws' // 生产环境:/api(nginx入口) + /ws(后端路由)
)
const RECONNECT_DELAY = 2000 const RECONNECT_DELAY = 2000
const MAX_RECONNECT_DELAY = 10000 const MAX_RECONNECT_DELAY = 10000
......
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const prisma = require('../prisma/client'); const prisma = require('../prisma/client');
const { getWaitingRoomTeamInfo } = require('../socket/roomHandler');
// GET /api/rooms — 获取所有房间列表(进行中房间含当前分数) // GET /api/rooms — 获取所有房间列表(进行中房间含当前分数)
router.get('/', async (_req, res, next) => { router.get('/', async (_req, res, next) => {
...@@ -44,7 +45,8 @@ router.get('/:roomId', async (req, res, next) => { ...@@ -44,7 +45,8 @@ router.get('/:roomId', async (req, res, next) => {
}); });
// GET /api/rooms/:roomId/check — 校验房间是否存在且处于等待中 // GET /api/rooms/:roomId/check — 校验房间是否存在且处于等待中
// 返回:{ ok: bool, roomId, totalSeats, joinedCount, message? } // 返回:{ ok: bool, roomId, totalSeats, perTeamSeats, teamA, teamB, message? }
// teamA / teamB: 当前各队人数;perTeamSeats: 每队座位上限(totalSeats / 2)
router.get('/:roomId/check', async (req, res, next) => { router.get('/:roomId/check', async (req, res, next) => {
try { try {
const { roomId } = req.params; const { roomId } = req.params;
...@@ -60,7 +62,13 @@ router.get('/:roomId/check', async (req, res, next) => { ...@@ -60,7 +62,13 @@ router.get('/:roomId/check', async (req, res, next) => {
return res.json({ ok: false, message: room.status === 'playing' ? '房间已开始游戏' : '房间已结束' }); return res.json({ ok: false, message: room.status === 'playing' ? '房间已开始游戏' : '房间已结束' });
} }
res.json({ ok: true, roomId, totalSeats: room.totalSeats }); // 从内存等待表中获取实时队伍人数
const teamInfo = getWaitingRoomTeamInfo(roomId);
const perTeamSeats = teamInfo?.perTeamSeats ?? Math.floor(room.totalSeats / 2);
const teamA = teamInfo?.teamA ?? 0;
const teamB = teamInfo?.teamB ?? 0;
res.json({ ok: true, roomId, totalSeats: room.totalSeats, perTeamSeats, teamA, teamB });
} catch (err) { } catch (err) {
next(err); next(err);
} }
......
...@@ -400,4 +400,21 @@ function _clearRoomTimer(roomId) { ...@@ -400,4 +400,21 @@ function _clearRoomTimer(roomId) {
} }
} }
module.exports = { registerRoomHandlers, onRoomEmpty }; /**
* 查询等待中房间的队伍人数情况
* @param {string} roomId
* @returns {{ teamA: number, teamB: number, totalSeats: number, perTeamSeats: number } | null}
* 返回 null 表示房间不在等待表中(未创建或已开始)
*/
function getWaitingRoomTeamInfo(roomId) {
const waiting = waitingRooms.get(String(roomId));
if (!waiting) return null;
const teamA = waiting.players.filter(p => p.team === 'A').length;
const teamB = waiting.players.filter(p => p.team === 'B').length;
const perTeamSeats = Math.floor(waiting.totalSeats / 2);
return { teamA, teamB, totalSeats: waiting.totalSeats, perTeamSeats };
}
module.exports = { registerRoomHandlers, onRoomEmpty, getWaitingRoomTeamInfo };
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论