diff --git a/CLAUDE.md b/CLAUDE.md
index 6f8f4e7..79797ce 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -18,9 +18,8 @@ cd frontend && npm run build
cd frontend && npm run dev
# 部署脚本(根目录)
-./deploy-backend.sh # 部署 PocketBase + LiveKit + Voice Token 后端
-./deploy-dev.sh # 构建 + 部署 Dev 前端 (端口 7033)
-./deploy-uat.sh # 构建 + 部署 UAT 前端 + 后端 (端口 7034/8712)
+./deploy-dev.sh # 构建 + 部署 Dev 全套 (PB + LiveKit + 前端, 端口 7033/8090)
+./deploy-uat.sh # 构建 + 部署 UAT 全套 (PB + LiveKit + 前端, 端口 7034/8712)
./stop-all.sh # 停止所有服务
# Electron 桌面端
@@ -31,11 +30,14 @@ cd electron && npm run start:uat # 连接 UAT 环境
cd electron && npm run build # 打包 Windows 可执行文件
# 查看日志
-docker logs -f gamegroup-pb # 后端
-docker logs -f gamegroup-livekit # LiveKit 语音服务
-docker logs -f gamegroup-voice-token # Voice Token 服务
-docker logs -f gamegroup-frontend-dev # Dev
-docker logs -f gamegroup-frontend-uat # UAT
+docker logs -f gamegroup-pb # Dev PocketBase
+docker logs -f gamegroup-pb-uat # UAT PocketBase
+docker logs -f gamegroup-livekit-dev # Dev LiveKit
+docker logs -f gamegroup-livekit-uat # UAT LiveKit
+docker logs -f gamegroup-voice-token-dev # Dev Voice Token
+docker logs -f gamegroup-voice-token-uat # UAT Voice Token
+docker logs -f gamegroup-frontend-dev # Dev 前端
+docker logs -f gamegroup-frontend-uat # UAT 前端
```
**重要**: 不要在本地启动 vite dev server,使用 Docker 部署后通过端口访问测试。Dev 环境在 `http://192.168.1.14:7033`。部署到 UAT 前必须等用户确认。
@@ -56,10 +58,10 @@ docker logs -f gamegroup-frontend-uat # UAT
|------|-----|-----|
| 前端 (nginx) | 7033 | 7034 |
| PocketBase | 8090 | 8712 |
-| LiveKit | 7880/7881/7882 | 7880/7881/7882 |
-| Voice Token | 7882 | 7882 |
+| LiveKit | 7880/7881/7882(udp) | 7890/7891/7892(udp) |
+| Voice Token | 7883 | 7893 |
-Docker Compose 文件:`docker-compose.backend.yml`、`docker-compose.dev.yml`、`docker-compose.uat.yml`,共享 `gamegroup-net` 网络。
+Docker Compose 文件:`docker-compose.dev.yml`(Dev 全套)、`docker-compose.uat.yml`(UAT 全套),共享 `gamegroup-net` 网络。每个环境独立运行完整的 PB + LiveKit + Voice Token + 前端服务。
前端 `.env` 文件:`frontend/.env.dev`(VITE_PB_URL=8711, PORT=7033)和 `frontend/.env.uat`(VITE_PB_URL=8711, PORT=7034)。注意 Dev/UAT 构建时 VITE_PB_URL 都指向 8711,因为 Docker 构建时会将其覆盖为空,由 nginx 反向代理处理。
@@ -76,7 +78,7 @@ pocketbase.ts (PB 客户端初始化)
```
- **`api/pocketbase.ts`** — 单例 PocketBase 客户端,导出 `pb`、`getCurrentUser()`、`isAuthenticated()`、`logout()`
-- **`api/`** — 每个领域一个文件(`users.ts`, `groups.ts`, `sessions.ts`, `invitations.ts`, `games.ts`, `polls.ts`, `bets.ts`, `points.ts`, `ledgers.ts`, `assets.ts`, `memories.ts`, `notifications.ts`, `gameBlacklist.ts`, `playerBlacklist.ts`, `voice.ts`)
+- **`api/`** — 每个领域一个文件(`users.ts`, `groups.ts`, `sessions.ts`, `invitations.ts`, `games.ts`, `polls.ts`, `bets.ts`, `points.ts`, `ledgers.ts`, `assets.ts`, `memories.ts`, `notifications.ts`, `gameBlacklist.ts`, `playerBlacklist.ts`, `voice.ts`, `bulletins.ts`)
- **`stores/`** — Pinia stores,组合式 API 风格(`defineStore('name', () => {...})`)
- **`composables/useRealtime.ts`** — 统一管理 PocketBase 实时订阅,组件卸载时自动清理
- **`composables/useVoiceRoom.ts`** — LiveKit 语音房间封装,处理连接/断开/麦克风/扬声器控制
@@ -94,7 +96,7 @@ Layout (`/`) 下所有认证页面为子路由:Home, GroupView (`/group/:id`),
### Vite 代理 vs Nginx
-开发环境 Vite 将 `/api` 代理到 PocketBase(去掉 `/api` 前缀)。生产环境 nginx 做同样代理,SSE realtime 连接额外禁用 buffering。Voice Token 服务通过 `/voice-api/` 路径由 nginx 代理到 7882 端口。
+开发环境 Vite 将 `/api` 代理到 PocketBase(去掉 `/api` 前缀)。生产环境 nginx 做同样代理,SSE realtime 连接额外禁用 buffering。Voice Token 服务通过 `/voice-api/` 路径由 nginx 代理到 voice-token 端口(Dev: 7883, UAT: 7893)。
前端 nginx 配置有两份:`nginx.conf`(dev,代理到 8090)和 `nginx.uat.conf`(代理到 8712),Docker 构建时通过 `NGINX_CONF` build arg 选择。静态资源缓存一年,HTML 不缓存。
@@ -105,7 +107,7 @@ Layout (`/`) 下所有认证页面为子路由:Home, GroupView (`/group/:id`),
2. `useVoiceRoom.connect(sessionId)` 调用 `api/voice.ts` 的 `fetchVoiceToken`
3. `fetchVoiceToken` 向后端 `/voice-api/voice-token/:sessionId` 请求 token
4. Voice Token 服务验证用户 PB token → 查询 `team_sessions` 确认成员身份 → 签发 LiveKit JWT
-5. 前端用 token 连接 LiveKit server (`ws://192.168.1.14:7880`)
+5. 前端用 token 连接 LiveKit server(Dev: `ws://192.168.1.14:7880`, UAT: `ws://192.168.1.14:7890`)
HTTP 环境下 `navigator.mediaDevices` 受限,已在 `useVoiceRoom.ts` 中给出明确的 Chrome flags 引导错误提示。
@@ -138,6 +140,7 @@ HTTP 环境下 `navigator.mediaDevices` 受限,已在 `useVoiceRoom.ts` 中给
- **game_blacklist** — 游戏黑名单(行为/外挂/坑货/环境差)
- **player_blacklist** — 玩家黑名单(标签:挂机/送人头/喷人等)
- **notifications** — 站内通知(投票/组队/入群等事件)
+- **bulletin_posts** / **bulletin_reads** — 群组信息公示板,支持置顶/优先级/已读追踪/过期时间
### PocketBase 注意事项
diff --git a/backend/pb_migrations/1776700000_created_bulletin_posts.js b/backend/pb_migrations/1776700000_created_bulletin_posts.js
deleted file mode 100644
index 6e4748e..0000000
--- a/backend/pb_migrations/1776700000_created_bulletin_posts.js
+++ /dev/null
@@ -1,122 +0,0 @@
-///
-migrate((db) => {
- const collection = new Collection({
- "id": "bulletin_posts",
- "created": "2026-04-21 00:00:00.000Z",
- "updated": "2026-04-21 00:00:00.000Z",
- "name": "bulletin_posts",
- "type": "base",
- "system": false,
- "schema": [
- {
- "system": false,
- "id": "bp_group",
- "name": "group",
- "type": "relation",
- "required": true,
- "presentable": false,
- "unique": false,
- "options": {
- "collectionId": "es63bkyiblpnxdf",
- "cascadeDelete": true,
- "minSelect": null,
- "maxSelect": 1,
- "displayFields": null
- }
- },
- {
- "system": false,
- "id": "bp_creator",
- "name": "creator",
- "type": "relation",
- "required": true,
- "presentable": false,
- "unique": false,
- "options": {
- "collectionId": "_pb_users_auth_",
- "cascadeDelete": true,
- "minSelect": null,
- "maxSelect": 1,
- "displayFields": null
- }
- },
- {
- "system": false,
- "id": "bp_title",
- "name": "title",
- "type": "text",
- "required": true,
- "presentable": false,
- "unique": false,
- "options": {
- "min": 1,
- "max": 200,
- "pattern": ""
- }
- },
- {
- "system": false,
- "id": "bp_content",
- "name": "content",
- "type": "text",
- "required": true,
- "presentable": false,
- "unique": false,
- "options": {
- "min": 1,
- "max": 5000,
- "pattern": ""
- }
- },
- {
- "system": false,
- "id": "bp_priority",
- "name": "priority",
- "type": "select",
- "required": true,
- "presentable": false,
- "unique": false,
- "options": {
- "maxSelect": 1,
- "values": ["low", "normal", "high", "urgent"]
- }
- },
- {
- "system": false,
- "id": "bp_pinned",
- "name": "pinned",
- "type": "bool",
- "required": false,
- "presentable": false,
- "unique": false,
- "options": {}
- },
- {
- "system": false,
- "id": "bp_expires_at",
- "name": "expiresAt",
- "type": "date",
- "required": false,
- "presentable": false,
- "unique": false,
- "options": {
- "min": "",
- "max": ""
- }
- }
- ],
- "indexes": [],
- "listRule": "@request.auth.id != \"\" && group.members ~ @request.auth.id",
- "viewRule": "@request.auth.id != \"\" && group.members ~ @request.auth.id",
- "createRule": "@request.auth.id != \"\" && group.members ~ @request.auth.id",
- "updateRule": "creator = @request.auth.id || group.owner = @request.auth.id",
- "deleteRule": "creator = @request.auth.id || group.owner = @request.auth.id",
- "options": {}
- });
-
- return Dao(db).saveCollection(collection);
-}, (db) => {
- const dao = new Dao(db);
- const collection = dao.findCollectionByNameOrId("bulletin_posts");
- return dao.deleteCollection(collection);
-})
diff --git a/backend/pb_migrations/1776700001_created_bulletin_reads.js b/backend/pb_migrations/1776700001_created_bulletin_reads.js
deleted file mode 100644
index 12162aa..0000000
--- a/backend/pb_migrations/1776700001_created_bulletin_reads.js
+++ /dev/null
@@ -1,60 +0,0 @@
-///
-migrate((db) => {
- const collection = new Collection({
- "id": "bulletin_reads",
- "created": "2026-04-21 00:00:01.000Z",
- "updated": "2026-04-21 00:00:01.000Z",
- "name": "bulletin_reads",
- "type": "base",
- "system": false,
- "schema": [
- {
- "system": false,
- "id": "br_post",
- "name": "post",
- "type": "relation",
- "required": true,
- "presentable": false,
- "unique": false,
- "options": {
- "collectionId": "bulletin_posts",
- "cascadeDelete": true,
- "minSelect": null,
- "maxSelect": 1,
- "displayFields": null
- }
- },
- {
- "system": false,
- "id": "br_user",
- "name": "user",
- "type": "relation",
- "required": true,
- "presentable": false,
- "unique": false,
- "options": {
- "collectionId": "_pb_users_auth_",
- "cascadeDelete": true,
- "minSelect": null,
- "maxSelect": 1,
- "displayFields": null
- }
- }
- ],
- "indexes": [
- "CREATE UNIQUE INDEX `idx_bulletin_reads_post_user` ON `bulletin_reads` (\n `post`,\n `user`\n)"
- ],
- "listRule": "@request.auth.id != \"\" && user = @request.auth.id",
- "viewRule": "@request.auth.id != \"\" && user = @request.auth.id",
- "createRule": "@request.auth.id != \"\" && user = @request.auth.id",
- "updateRule": null,
- "deleteRule": "user = @request.auth.id || post.group.owner = @request.auth.id",
- "options": {}
- });
-
- return Dao(db).saveCollection(collection);
-}, (db) => {
- const dao = new Dao(db);
- const collection = dao.findCollectionByNameOrId("bulletin_reads");
- return dao.deleteCollection(collection);
-})
diff --git a/backend/pb_migrations/1776749288_updated_game_blacklist.js b/backend/pb_migrations/1776749288_updated_game_blacklist.js
new file mode 100644
index 0000000..8f6c39f
--- /dev/null
+++ b/backend/pb_migrations/1776749288_updated_game_blacklist.js
@@ -0,0 +1,16 @@
+///
+migrate((db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("gblacklist_col")
+
+ collection.updateRule = "reporter = @request.auth.id"
+
+ return dao.saveCollection(collection)
+}, (db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("gblacklist_col")
+
+ collection.updateRule = null
+
+ return dao.saveCollection(collection)
+})
diff --git a/deploy-backend.sh b/deploy-backend.sh
deleted file mode 100644
index 6c18e47..0000000
--- a/deploy-backend.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/bash
-# 部署 PocketBase 后端
-
-echo "🚀 部署 PocketBase 后端..."
-
-docker compose -f docker-compose.backend.yml up -d --force-recreate
-
-echo "✅ PocketBase 已启动"
-echo "📡 API 地址: http://192.168.1.14:8711/api/"
-echo "🔧 管理面板: http://192.168.1.14:8711/_/"
diff --git a/deploy-dev.sh b/deploy-dev.sh
index c630f82..d885349 100644
--- a/deploy-dev.sh
+++ b/deploy-dev.sh
@@ -1,12 +1,20 @@
#!/bin/bash
-# 部署 Dev 前端
+# 部署 Dev 全套环境(PocketBase + LiveKit + voice-token + 前端)
-echo "🚀 部署 Dev 前端..."
+echo "🚀 部署 Dev 环境..."
# 确保网络存在
docker network create gamegroup-net 2>/dev/null || true
+# 先停掉旧容器(含已改名的 livekit / voice-token)
+docker rm -f gamegroup-livekit gamegroup-voice-token 2>/dev/null || true
+
docker compose -f docker-compose.dev.yml up -d --build --force-recreate
+echo ""
echo "✅ Dev 环境已启动"
-echo "🌐 访问地址: http://192.168.1.14:7033"
+echo "🌐 前端: http://192.168.1.14:7033"
+echo "📡 PB API: http://192.168.1.14:8090/api/"
+echo "🔧 PB 管理面板: http://192.168.1.14:8090/_/"
+echo "🎙️ LiveKit: ws://192.168.1.14:7880"
+echo "🔑 Token: http://192.168.1.14:7883"
diff --git a/deploy-uat.sh b/deploy-uat.sh
index 516eeee..45ea89d 100644
--- a/deploy-uat.sh
+++ b/deploy-uat.sh
@@ -1,12 +1,20 @@
#!/bin/bash
-# 部署 UAT 前端
+# 部署 UAT 全套环境(PocketBase + LiveKit + voice-token + 前端)
-echo "🚀 部署 UAT 前端..."
+echo "🚀 部署 UAT 环境..."
# 确保网络存在
docker network create gamegroup-net 2>/dev/null || true
+# 先停掉旧容器(含已改名的 livekit / voice-token)
+docker rm -f gamegroup-livekit gamegroup-voice-token 2>/dev/null || true
+
docker compose -f docker-compose.uat.yml up -d --build --force-recreate
+echo ""
echo "✅ UAT 环境已启动"
-echo "🌐 访问地址: http://192.168.1.14:7034"
+echo "🌐 前端: http://192.168.1.14:7034"
+echo "📡 PB API: http://192.168.1.14:8712/api/"
+echo "🔧 PB 管理面板: http://192.168.1.14:8712/_/"
+echo "🎙️ LiveKit: ws://192.168.1.14:7890"
+echo "🔑 Token: http://192.168.1.14:7893"
diff --git a/docker-compose.backend.yml b/docker-compose.backend.yml
deleted file mode 100644
index 8c7a2ea..0000000
--- a/docker-compose.backend.yml
+++ /dev/null
@@ -1,55 +0,0 @@
-services:
- pocketbase:
- image: ghcr.io/muchobien/pocketbase:0.22.4
- container_name: gamegroup-pb
- ports:
- - "8090:8090"
- volumes:
- - ./backend/pb_data:/pb_data
- - ./backend/pb_migrations:/pb_migrations
- - ./backend/pb_hooks:/pb_hooks
- environment:
- - GO_ENV=production
- restart: unless-stopped
- healthcheck:
- test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8090/api/health"]
- interval: 30s
- timeout: 10s
- retries: 3
- start_period: 40s
- networks:
- - gamegroup-net
-
- livekit:
- image: livekit/livekit-server:v1.10
- container_name: gamegroup-livekit
- ports:
- - "7880:7880"
- - "7881:7881/udp"
- - "7882:7882/udp"
- environment:
- LIVEKIT_KEYS: "APIyxZGQjM2: secretNmU4ZDU3YjA0OWIxNDM4YjhlNWY3YTFjZGUzOWRi"
- command: --dev --node-ip 192.168.1.14
- restart: unless-stopped
- networks:
- - gamegroup-net
-
- voice-token:
- build:
- context: ./backend/voice-token-service
- container_name: gamegroup-voice-token
- ports:
- - "7882:7882"
- environment:
- - LIVEKIT_API_KEY=APIyxZGQjM2
- - LIVEKIT_API_SECRET=secretNmU4ZDU3YjA0OWIxNDM4YjhlNWY3YTFjZGUzOWRi
- - PB_URL=http://gamegroup-pb:8090
- restart: unless-stopped
- depends_on:
- - pocketbase
- networks:
- - gamegroup-net
-
-networks:
- gamegroup-net:
- driver: bridge
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 59750f6..58761fe 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -1,4 +1,55 @@
services:
+ pocketbase-dev:
+ image: ghcr.io/muchobien/pocketbase:0.22.4
+ container_name: gamegroup-pb
+ ports:
+ - "8090:8090"
+ volumes:
+ - ./backend/pb_data:/pb_data
+ - ./backend/pb_migrations:/pb_migrations
+ - ./backend/pb_hooks:/pb_hooks
+ environment:
+ - GO_ENV=production
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8090/api/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+ networks:
+ - gamegroup-net
+
+ livekit-dev:
+ image: livekit/livekit-server:v1.10
+ container_name: gamegroup-livekit-dev
+ ports:
+ - "7880:7880"
+ - "7881:7881/udp"
+ - "7882:7882/udp"
+ environment:
+ LIVEKIT_KEYS: "APIyxZGQjM2: secretNmU4ZDU3YjA0OWIxNDM4YjhlNWY3YTFjZGUzOWRi"
+ command: --dev --node-ip 192.168.1.14
+ restart: unless-stopped
+ networks:
+ - gamegroup-net
+
+ voice-token-dev:
+ build:
+ context: ./backend/voice-token-service
+ container_name: gamegroup-voice-token-dev
+ ports:
+ - "7883:7882"
+ environment:
+ - LIVEKIT_API_KEY=APIyxZGQjM2
+ - LIVEKIT_API_SECRET=secretNmU4ZDU3YjA0OWIxNDM4YjhlNWY3YTFjZGUzOWRi
+ - PB_URL=http://gamegroup-pb:8090
+ restart: unless-stopped
+ depends_on:
+ - pocketbase-dev
+ networks:
+ - gamegroup-net
+
frontend-dev:
build:
context: ./frontend
@@ -11,6 +62,8 @@ services:
environment:
- NODE_ENV=production
restart: unless-stopped
+ depends_on:
+ - pocketbase-dev
networks:
- gamegroup-net
diff --git a/docker-compose.uat.yml b/docker-compose.uat.yml
index b14a0a4..d880d4f 100644
--- a/docker-compose.uat.yml
+++ b/docker-compose.uat.yml
@@ -20,13 +20,13 @@ services:
networks:
- gamegroup-net
- livekit:
+ livekit-uat:
image: livekit/livekit-server:v1.10
- container_name: gamegroup-livekit
+ container_name: gamegroup-livekit-uat
ports:
- - "7880:7880"
- - "7881:7881/udp"
- - "7882:7882/udp"
+ - "7890:7880"
+ - "7891:7881/udp"
+ - "7892:7882/udp"
environment:
LIVEKIT_KEYS: "APIyxZGQjM2: secretNmU4ZDU3YjA0OWIxNDM4YjhlNWY3YTFjZGUzOWRi"
command: --dev --node-ip 192.168.1.14
@@ -34,12 +34,12 @@ services:
networks:
- gamegroup-net
- voice-token:
+ voice-token-uat:
build:
context: ./backend/voice-token-service
- container_name: gamegroup-voice-token
+ container_name: gamegroup-voice-token-uat
ports:
- - "7882:7882"
+ - "7893:7882"
environment:
- LIVEKIT_API_KEY=APIyxZGQjM2
- LIVEKIT_API_SECRET=secretNmU4ZDU3YjA0OWIxNDM4YjhlNWY3YTFjZGUzOWRi
diff --git a/frontend/.env.dev b/frontend/.env.dev
index 5b40e97..6658342 100644
--- a/frontend/.env.dev
+++ b/frontend/.env.dev
@@ -2,4 +2,4 @@
VITE_PB_URL=http://192.168.1.14:8711
VITE_PORT=7033
VITE_LIVEKIT_URL=ws://192.168.1.14:7880
-VITE_VOICE_TOKEN_URL=http://192.168.1.14:7882
+VITE_VOICE_TOKEN_URL=http://192.168.1.14:7883
diff --git a/frontend/.env.uat b/frontend/.env.uat
index 7181004..8a80ce4 100644
--- a/frontend/.env.uat
+++ b/frontend/.env.uat
@@ -1,5 +1,5 @@
# UAT Environment
VITE_PB_URL=http://192.168.1.14:8711
VITE_PORT=7034
-VITE_LIVEKIT_URL=ws://192.168.1.14:7880
-VITE_VOICE_TOKEN_URL=http://192.168.1.14:7882
+VITE_LIVEKIT_URL=ws://192.168.1.14:7890
+VITE_VOICE_TOKEN_URL=http://192.168.1.14:7893
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
index 836a995..df8bc4e 100644
--- a/frontend/nginx.conf
+++ b/frontend/nginx.conf
@@ -32,7 +32,7 @@ server {
# Voice token service proxy
location /voice-api/ {
- proxy_pass http://192.168.1.14:7882/api/;
+ proxy_pass http://192.168.1.14:7883/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
diff --git a/frontend/nginx.uat.conf b/frontend/nginx.uat.conf
index c6095b6..fb55ea6 100644
--- a/frontend/nginx.uat.conf
+++ b/frontend/nginx.uat.conf
@@ -32,7 +32,7 @@ server {
# Voice token service proxy
location /voice-api/ {
- proxy_pass http://192.168.1.14:7882/api/;
+ proxy_pass http://192.168.1.14:7893/api/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
diff --git a/stop-all.sh b/stop-all.sh
index 57e938c..9182244 100644
--- a/stop-all.sh
+++ b/stop-all.sh
@@ -3,8 +3,10 @@
echo "🛑 停止所有服务..."
-docker compose -f docker-compose.backend.yml down
docker compose -f docker-compose.dev.yml down
docker compose -f docker-compose.uat.yml down
+# 清理旧名称的残留容器
+docker rm -f gamegroup-livekit gamegroup-voice-token 2>/dev/null || true
+
echo "✅ 所有服务已停止"