问题8 登录系统: - node:sqlite 账号存储(scrypt 哈希 + HMAC 签名 token),preset admin 改为 e2hang/evan1115 - /api/auth/register|login|me,me 返回 room 供重联 - 网关拦截 create/join 需登录(401),列表保持开放 踢人逻辑修正(问题2 延伸): - kickMove 真正释放座位(folded=false, name=null)而非锁死 - war/mill 去掉永久封禁语义 管理员越权:管理员 token 可删任意房间、踢任意房间内的人 单房间约束(用户新需求): - 同一用户同时只能在一个房间,create/join 他房返回 409 - 踢人后若房间仅剩踢人者则自动解散并释放其记录(修复 2 人局踢光对手后被困 409) - 陈旧房间记录兜底清理 UI 清理:移除"改名"按钮与"你的名字"输入框;删除房间按钮仅房主/管理员可见 docs: 新增 12-auth-kick-single-room.md 汇总本会话全部需求/根因/修复/边界测试 Co-Authored-By: Claude (CodeBuddy) <noreply@codebuddy.ai>
5.7 KiB
5.7 KiB
问题 2:删除房间 / 房主心跳 / 房主退出自动关房 / 踢人
通用问题:所有对局(War / Mill / Holdem / DragTest)都适用,因为房间管理在 boardgame.io Lobby 层,与具体游戏无关。
现状(boardgame.io 0.50.2 的能力边界)
- Lobby REST 只有
create / join / leave / playAgain / update,没有 delete / heartbeat / kick。 leave:最后一个玩家离开时db.wipe(matchID)删房间(server.js:2331);否则仅清该玩家 name/credentials。- 没有在线检测 / 心跳:server 不知道谁“还连着”。客户端断开 socket 不会自动 leave,房间会一直挂着。
- 房主定义:本项目约定
playerID '0'(创建者)即房主(holdem.ts的host()取最低编号 seated 且有筹码者,联机下等价于 P0)。
需求拆解
- 删除房间:房主有权删除;删除后所有客户端应被踢出 / 房间消失。
- 房主退出自动关房:房主无 heartbeat(断开)超过 10min,房间自动解除。
- 心跳:房主心跳周期长(10min);为支持“自动关房”,需要一个心跳端点。
- 踢人:房主有权把某个玩家踢出房间。
拟采用方案(在 packages/server 加自定义 koa 路由)
利用 Server() 返回的 server.app / server.db / server.auth,在 index.ts 里挂一组 /api/rooms/... 路由。
房主鉴权:请求携带 playerID=0 + credentials(与 lobby 一致),用 server.auth.authenticateCredentials 校验。
2.1 内存在线表(心跳)
// 房间 -> { pid: lastSeenEpochMs }
const presence = new Map<string, Record<string, number>>();
const HOST_TIMEOUT_MS = 10 * 60 * 1000; // 房主 10min
const PLAYER_TIMEOUT_MS = 3 * 60 * 1000;
POST /api/rooms/:id/heartbeat:body{ playerID, credentials },鉴权后presence[matchID][playerID] = Date.now()。 所有玩家都建议定时 ping(房主 10min 一次,普通玩家可更频繁),至少房主必须 ping。- 定时器(server 启动时
setInterval,如每 60s):扫描 presence, 若房主(playerID '0'或当前 host)超时 → 执行“自动关房”(同删除逻辑)。
2.2 删除房间
POST /api/rooms/:id/delete body: { playerID, credentials }
仅房主可删。逻辑:
const { metadata } = await server.db.fetch(matchID, { metadata: true });
// 校验 host 凭证
await server.db.wipe(matchID);
presence.delete(matchID);
// 通知所有 socket 客户端(通过 pubSub / socket.io)房间已删
客户端在 LobbyRoomList 的房间列表轮询(已有 refresh)会自然发现房间消失;
另可在 boardgame.io 的 onUpdate/自定义事件里让已挂载的 Client 监听到“房间被删”并回退到列表。
2.3 踢人
POST /api/rooms/:id/kick body: { playerID: hostId, credentials, target: targetPid }
仅房主可踢。逻辑:
// 1) lobby metadata 清掉 target 的 name/credentials
metadata.players[target].name = undefined;
metadata.players[target].credentials = undefined;
await server.db.setMetadata(matchID, metadata);
// 2) 在游戏状态 G 里把该座位 seated=false(需发一个 game move 或走 master)
// 简化:用 master 的 dispatch 调一个 engine move `kickMove(target)`
// 3) presence 清掉 target
- 关键难点:踢人既要改 lobby metadata(让座位空出来可被别人加入),又要改游戏内 G(让被踢者从牌桌消失)。
改 G 需经 boardgame.io 的
Masterdispatch(不能直接改 db 状态,否则客户端状态漂移)。 方案:在每个游戏里加一个kickmove(通用约定),server 用Master触发它。 为减小改动,可定义一个通用插件/约定:所有游戏实现kick(targetPid)move, 删房间时也对所有人调用。
2.4 房主退出自动关房(合并到 2.1 定时器)
定时器发现房主超时 → 等同“删除房间”。
客户端改动(ui / web)
LobbyRoomList:- 房主视角:每个房间 pill 增加“删除房间”按钮(调
delete); 房间内当前玩家列表增加“踢”按钮(调kick)。 - 挂载后启动
setInterval调heartbeat(房主 10min;普通玩家按 PLAYER_TIMEOUT 的一半)。
- 房主视角:每个房间 pill 增加“删除房间”按钮(调
App.tsx/HoldemBoard:无需特殊处理,房间消失后列表刷新即可;可加“房间已被删除”提示。
风险与决策点
- Master dispatch 改 G:需要拿到
server的 master 实例。boardgame.ioServer()没直接暴露master, 但可通过server.db+transport重新构造,或复用router内部的db。 备选(更简单):把“座位空出 / 踢人”完全放进 lobby metadata,游戏内playerView/move 读取 metadata? 不行(G 拿不到 metadata)。因此必须经一个游戏内 move。 → 建议:每个游戏加kickmove(空实现也行,至少清seated),server 用db+ 自定义 dispatch。 - 是否现在就给所有游戏加 kick move:会影响 War/Mill/DragTest。可先做 Holdem + 通用骨架,其余游戏加最小
kick。 - 房主判定:当前
host()取“最低编号 seated 且有筹码者”,联机下创建者 = P0,但若 P0 破产/离开,host 会漂移。 建议把“房主”显式固化在metadata(创建者 playerID),server 端以 metadata 的createdBy为准,enginehost()也改为读 G 里的hostId(创建时写死)。
验证
- 房主删除 → 列表消失、其他客户端刷新后无此房间、已挂载 Client 收到“已删除”。
- 房主断网 10min(测试用短超时)→ 房间被自动 wipe。
- 房主踢人 → 被踢者 metadata 清、G 里 seated=false、座位可被重新加入。
- 非房主调 delete/kick → 403。