问题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>
94 lines
5.7 KiB
Markdown
94 lines
5.7 KiB
Markdown
# 问题 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)。
|
||
|
||
## 需求拆解
|
||
1. **删除房间**:房主有权删除;删除后所有客户端应被踢出 / 房间消失。
|
||
2. **房主退出自动关房**:房主无 heartbeat(断开)超过 10min,房间自动解除。
|
||
3. **心跳**:房主心跳周期长(10min);为支持“自动关房”,需要一个心跳端点。
|
||
4. **踢人**:房主有权把某个玩家踢出房间。
|
||
|
||
## 拟采用方案(在 `packages/server` 加自定义 koa 路由)
|
||
|
||
利用 `Server()` 返回的 `server.app / server.db / server.auth`,在 `index.ts` 里挂一组 `/api/rooms/...` 路由。
|
||
房主鉴权:请求携带 `playerID=0` + `credentials`(与 lobby 一致),用 `server.auth.authenticateCredentials` 校验。
|
||
|
||
### 2.1 内存在线表(心跳)
|
||
```ts
|
||
// 房间 -> { 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 }
|
||
```
|
||
仅房主可删。逻辑:
|
||
```ts
|
||
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 }
|
||
```
|
||
仅房主可踢。逻辑:
|
||
```ts
|
||
// 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 的 `Master` dispatch(不能直接改 db 状态,否则客户端状态漂移)。
|
||
方案:在**每个游戏**里加一个 `kick` move(通用约定),server 用 `Master` 触发它。
|
||
为减小改动,可定义一个**通用插件/约定**:所有游戏实现 `kick(targetPid)` move,
|
||
删房间时也对所有人调用。
|
||
|
||
### 2.4 房主退出自动关房(合并到 2.1 定时器)
|
||
定时器发现房主超时 → 等同“删除房间”。
|
||
|
||
## 客户端改动(ui / web)
|
||
- `LobbyRoomList`:
|
||
- 房主视角:每个房间 pill 增加“删除房间”按钮(调 `delete`);
|
||
房间内当前玩家列表增加“踢”按钮(调 `kick`)。
|
||
- 挂载后启动 `setInterval` 调 `heartbeat`(房主 10min;普通玩家按 PLAYER_TIMEOUT 的一半)。
|
||
- `App.tsx` / `HoldemBoard`:无需特殊处理,房间消失后列表刷新即可;可加“房间已被删除”提示。
|
||
|
||
## 风险与决策点
|
||
- **Master dispatch 改 G**:需要拿到 `server` 的 master 实例。boardgame.io `Server()` 没直接暴露 `master`,
|
||
但可通过 `server.db` + `transport` 重新构造,或复用 `router` 内部的 `db`。
|
||
**备选(更简单)**:把“座位空出 / 踢人”完全放进 lobby metadata,游戏内 `playerView`/move 读取 metadata?
|
||
不行(G 拿不到 metadata)。因此**必须**经一个游戏内 move。
|
||
→ 建议:每个游戏加 `kick` move(空实现也行,至少清 `seated`),server 用 `db` + 自定义 dispatch。
|
||
- **是否现在就给所有游戏加 kick move**:会影响 War/Mill/DragTest。可先做 Holdem + 通用骨架,其余游戏加最小 `kick`。
|
||
- **房主判定**:当前 `host()` 取“最低编号 seated 且有筹码者”,联机下创建者 = P0,但若 P0 破产/离开,host 会漂移。
|
||
建议把“房主”显式固化在 `metadata`(创建者 playerID),server 端以 metadata 的 `createdBy` 为准,engine `host()` 也改为读 G 里的 `hostId`(创建时写死)。
|
||
|
||
## 验证
|
||
- 房主删除 → 列表消失、其他客户端刷新后无此房间、已挂载 Client 收到“已删除”。
|
||
- 房主断网 10min(测试用短超时)→ 房间被自动 wipe。
|
||
- 房主踢人 → 被踢者 metadata 清、G 里 seated=false、座位可被重新加入。
|
||
- 非房主调 delete/kick → 403。
|