Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
P
paopao
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
cocktail-party
paopao
Commits
54b3cf18
提交
54b3cf18
authored
3月 19, 2026
作者:
lidongxu
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
增加队伍是否满员检测
上级
03a31ec1
显示空白字符变更
内嵌
并排
正在显示
4 个修改的文件
包含
249 行增加
和
27 行删除
+249
-27
index.js
admin/src/socket/index.js
+203
-23
socket.js
big-screen/src/socket.js
+18
-1
rooms.js
server/src/routes/rooms.js
+10
-2
roomHandler.js
server/src/socket/roomHandler.js
+18
-1
没有找到文件。
admin/src/socket/index.js
浏览文件 @
54b3cf18
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
=
''
)
{
if
(
socket
?.
connected
)
return
socket
const
url
=
baseURL
||
import
.
meta
.
env
.
VITE_SOCKET_URL
||
(
import
.
meta
.
env
.
DEV
?
window
.
location
.
origin
:
''
)
socket
=
io
(
url
,
{
path
:
'/socket.io'
,
transports
:
[
'websocket'
,
'polling'
],
autoConnect
:
true
,
// 环境切换:true=本地开发,false=线上生产
const
isDev
=
import
.
meta
.
env
.
DEV
||
window
.
location
.
hostname
===
'localhost'
// WebSocket URL
const
WS_URL
=
isDev
?
'ws://localhost:3000/ws'
// 开发环境
:
'wss://paopao.wxl66.cn/api/ws'
;
// 生产环境:/api(nginx入口) + /ws(后端路由)
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
()
})
return
socket
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
()
{
return
s
ocket
return
adminS
ocket
}
export
function
disconnectSocket
()
{
if
(
socket
)
{
socket
.
disconnect
()
socket
=
null
}
adminSocket
.
disconnect
()
}
export
function
onRoomState
(
cb
)
{
if
(
!
socket
)
return
socket
.
on
(
'room:state'
,
cb
)
adminSocket
.
on
(
'room:state'
,
cb
)
}
export
function
offRoomState
(
cb
)
{
if
(
!
socket
)
return
socket
.
off
(
'room:state'
,
cb
)
adminSocket
.
off
(
'room:state'
,
cb
)
}
export
function
onScreenRoomChanged
(
cb
)
{
if
(
!
socket
)
return
socket
.
on
(
'screen:roomChanged'
,
cb
)
adminSocket
.
on
(
'screen:roomChanged'
,
cb
)
}
export
function
offScreenRoomChanged
(
cb
)
{
if
(
!
socket
)
return
socket
.
off
(
'screen:roomChanged'
,
cb
)
adminSocket
.
off
(
'screen:roomChanged'
,
cb
)
}
export
default
adminSocket
big-screen/src/socket.js
浏览文件 @
54b3cf18
...
...
@@ -7,7 +7,24 @@
*/
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
MAX_RECONNECT_DELAY
=
10000
...
...
server/src/routes/rooms.js
浏览文件 @
54b3cf18
const
express
=
require
(
'express'
);
const
router
=
express
.
Router
();
const
prisma
=
require
(
'../prisma/client'
);
const
{
getWaitingRoomTeamInfo
}
=
require
(
'../socket/roomHandler'
);
// GET /api/rooms — 获取所有房间列表(进行中房间含当前分数)
router
.
get
(
'/'
,
async
(
_req
,
res
,
next
)
=>
{
...
...
@@ -44,7 +45,8 @@ router.get('/:roomId', async (req, res, next) => {
});
// 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
)
=>
{
try
{
const
{
roomId
}
=
req
.
params
;
...
...
@@ -60,7 +62,13 @@ router.get('/:roomId/check', async (req, res, next) => {
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
)
{
next
(
err
);
}
...
...
server/src/socket/roomHandler.js
浏览文件 @
54b3cf18
...
...
@@ -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
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论