# 10 · 部署后修复:LAN CORS + 重复 join 409 + 测试容忍 tie ## 做什么 把 09 那一轮交付的功能在真实 LAN 部署上跑通: - 服务端跑在 `192.168.5.11:8000` + vite dev `192.168.5.11:5173` - **本机 + 局域网其它机器**两个浏览器都能开网页 → 选 War → 联机 → 创房间/加房间 - 用户实测发现两个 bug,已修 ## 遇到什么问题 ### 问题 1:跨 LAN IP 访问 → fetch 失败 + 一直 connecting **症状**:用户在另一台机器开 `http://192.168.5.11:5173/`,点 "创建房间",浏览器报: ``` Access to fetch at 'http://localhost:8000/games/War?isGameover=false' from origin 'http://192.168.5.11:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. ``` socket.io 也连不上(页面底部一直显示 connecting)。 **根因**: ```ts // 09 那轮的 dev 默认 origins function parseOrigins(): (string | RegExp)[] { if (!process.env.ALLOWED_ORIGINS) { return [ /^https?:\/\/localhost(:\d+)?$/, // ← 只匹配 hostname 'localhost' /^https?:\/\/127\.0\.0\.1(:\d+)?$/, 'tauri://localhost', 'http://tauri.localhost', ]; } ... } ``` `192.168.5.11` 不在白名单 → `isOriginAllowed` 返回 false → `@koa/cors` 不发 `Access-Control-Allow-Origin` → 浏览器拦截响应。 **修法**(`packages/server/src/index.ts`): ```ts return [ /^http:\/\/localhost(:\d+)?$/, /^http:\/\/127\.0\.0\.1(:\d+)?$/, /^http:\/\/192\.168\.\d{1,3}\.\d{1,3}(:\d+)?$/, // LAN /^http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/, // LAN /^http:\/\/172\.(1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}(:\d+)?$/, // LAN /^http:\/\/100\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d+)?$/, // Tailscale/CGNAT /^https:\/\/localhost(:\d+)?$/, 'tauri://localhost', 'http://tauri.localhost', 'https://tauri.localhost', ]; ``` **验证**: ``` $ curl -I -X OPTIONS http://192.168.5.11:8000/games/War/create \ -H "Origin: http://192.168.5.11:5173" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type" HTTP/1.1 204 No Content Access-Control-Allow-Origin: http://192.168.5.11:5173 Access-Control-Allow-Methods: GET,HEAD,PUT,POST,DELETE,PATCH Access-Control-Allow-Headers: content-type ``` ### 问题 2:点 "加入 P1" → HTTP 409 "Player 1 not available" **症状**:用户能看到房间,但点 "加入 P1" 后 server 返回 409。 **根因(两重)**: **(2a)** 09 那轮修复时漏了:`LobbyRoomList.joinRoom` 成功后**没有刷新列表**。本地 `rooms` state 残留旧数据,"加入 P1" 按钮还显示着,但服务端 P1 已经被占 → 重复点就 409。 **(2b)** 测试/真人都可能碰上:server `InMemory` DB 残留上一次测试的旧房间。`lobby.refresh()` 按 `db.listMatches()` 顺序返回——插入顺序——所以 `.first()` 选中的可能是已满的旧房间。 **修法**: 1. `packages/ui/src/LobbyRoomList.tsx` — join 成功后刷新: ```ts onSelect(matchID, joinRes.playerID, joinRes.playerCredentials); await refresh(); // ← 新增:让 "加入 P1" 按钮自动隐藏 ``` 2. `apps/web/e2e/war-multiplayer.spec.ts` — 选"含加入 P1 按钮的房间"而不是 `.first()`: ```ts const firstJoinable = page.locator('[data-testid^="lobby-join-1-"]').first(); ``` 3. 新增回归测试 `after join, lobby list refreshes so "加入" button disappears`——验证 joinRoom 的 refresh() 行为。 ### 问题 3:测试遇到 tie 就死 **症状**:跑 war E2E 偶发失败 —— `collect` 后 pile 还是 1。 **根因**: - 09 修了 War 平局时 collect 保留 pile 的 bug(dev-log 09 §问题 4) - 服务端 `Math.random()` 洗牌不可预测,测试 deck 偶发平局 → collect 不收 → 测试断言 pile===0 失败 **修法**(`apps/web/e2e/war-multiplayer.spec.ts`):让测试容忍 tie: ```ts // 之前:断言一定是 0 await expect(p0.getByTestId('zone-count-p0-pile')).toHaveText('0', { timeout: 10_000 }); // 现在:断言是 0 或 1(tie 留着) await expect(p1.getByTestId('zone-count-p0-pile')).toHaveText(/^[01]$/, { timeout: 10_000 }); ``` ## 怎么解决(部署命令) ```bash # 1. build server pnpm --filter @tts-like/server build # 2. 起 server(绑定 0.0.0.0 让 LAN 可达) cd /home/e2hang/code/Projects/boardgame/tts-like/packages/server nohup node --import tsx src/index.ts > /tmp/tts-server.log 2>&1 & # 3. 起 vite(绑定 0.0.0.0) cd /home/e2hang/code/Projects/boardgame/tts-like/apps/web nohup pnpm dev --host 0.0.0.0 > /tmp/tts-web.log 2>&1 & ``` ## 成果 - Commit: 即将提交(`fix(multiplayer): LAN CORS + join refresh + tie-tolerant tests`) - 4/4 E2E 通过(含新加的 `after join ... no double-click 409` 回归测试) - server PID `1339755` 在 `:8000`,vite PID `1279130` 在 `:5173` ## 验证(已 curl) | 来源 | web | server | /games | CORS preflight | |---|---|---|---|---| | localhost | ✅ | ✅ | ✅ | ✅ | | 192.168.5.11 | ✅ | ✅ | ✅ | ✅ (新增 Allow-Origin: 192.168.5.11:5173) | ## 你的验收路径 1. 本机浏览器开 `http://192.168.5.11:5173/` (强制刷新 Ctrl+Shift+R 清缓存) 2. 选 War → 联机 → 创建房间 → 看到 "(你: 0)" 3. 另一台机器开 `http://192.168.5.11:5173/` → 选 War → 联机 → 刷新列表 → 加入 P1 4. 两边都能进游戏 → 一边翻牌 → 另一边同步 ## 关联 - [09-room-list-ui-and-complete-tests.md](./09-room-list-ui-and-complete-tests.md) — 上一轮的初始实现 - [lessons-learned.md §9.1](./lessons-learned.md) — '*' 不通配;新加 §10.1-10.3 - [decisions.md D24](./decisions.md) — dev origins 覆盖 LAN