diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..3012169
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,79 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## 项目概述
+
+Game Group V2 — 游戏组队管理平台。用户创建/加入群组,组队开黑,管理游戏库。
+
+## 开发命令
+
+```bash
+# 前端开发(默认连接 localhost:8090 的 PocketBase)
+cd frontend && npm run dev
+
+# 前端开发环境(连接远程后端 192.168.1.14:8711,端口 7033)
+cd frontend && npm run dev:dev
+
+# 前端 UAT 环境(端口 7034)
+cd frontend && npm run dev:uat
+
+# 构建前端
+cd frontend && npm run build
+
+# 启动后端(PocketBase,端口 8711)
+cd backend && docker-compose up -d
+
+# 部署脚本(根目录)
+./deploy-backend.sh # 部署后端
+./deploy-dev.sh # 部署 Dev 前端
+./deploy-uat.sh # 部署 UAT 前端
+./stop-all.sh # 停止所有服务
+```
+
+## 技术栈
+
+- **后端**: PocketBase 0.22.4 (Docker, `ghcr.io/muchobien/pocketbase`) — 无自定义 JS hooks,业务逻辑全在前端
+- **前端**: Vue 3 + TypeScript + Pinia + Element Plus + Tailwind CSS + Vite
+- **API 通信**: PocketBase JS SDK (`pocketbase` npm 包),cookie 认证
+- **实时通信**: PocketBase realtime subscriptions
+
+## 架构
+
+### 前端目录结构 (`frontend/src/`)
+
+- **`api/`** — 每个领域一个文件(`users.ts`, `groups.ts`, `sessions.ts`, `invitations.ts`, `games.ts`),封装 PocketBase CRUD 和过滤逻辑。`pocketbase.ts` 初始化客户端并导出认证工具函数
+- **`stores/`** — Pinia stores(`user`, `group`, `team`, `notification`),组合式 API 风格(`defineStore('name', () => {...})`)
+- **`composables/useRealtime.ts`** — 统一管理 PocketBase 实时订阅,组件卸载时自动清理
+- **`views/`** — 页面级组件,路由懒加载
+- **`components/`** — 按领域分子目录:`common/`, `game/`, `group/`, `layout/`, `team/`
+- **`types/index.ts`** — 所有 TypeScript 接口和类型定义集中在一个文件
+
+### 数据模型(PocketBase Collections)
+
+- **users** — 用户,含状态(idle/working/in_team/away)、工作时间设定、积分
+- **groups** — 群组,owner + members 关系,支持审核加入(requireApproval)
+- **team_sessions** — 临时组队,关联 sourceGroup,状态流转:recruiting → playing → finished/dissolved
+- **invitations** — 组队邀请,from/to 用户,pending/accepted/rejected
+- **games** — 游戏库,归属 group,含平台、标签、封面
+- **game_comments** / **game_favorites** — 游戏评论和收藏
+- **join_requests** — 入群申请,pending/approved/rejected
+
+### 关键模式
+
+- **Vite 代理**: 开发时 `/api` 代理到 PocketBase,路径重写去掉 `/api` 前缀(`vite.config.ts`)
+- **认证流程**: `pocketbase.ts` → cookie 持久化 → 路由守卫检查 `isAuthenticated()` → 未登录跳转 `/login`
+- **实时订阅**: 通过 `useRealtime` composable 和 `subscribe*` 函数订阅 PocketBase 变更事件,各 store 自行刷新数据
+- **样式系统**: 自定义 CSS 变量(`design.css`,`--gg-*` 前缀)+ Tailwind + Element Plus,主题色为绿色系
+
+### 后端
+
+- PocketBase 数据迁移在 `backend/pb_migrations/`,由管理面板操作自动生成
+- `backend/pb_hooks/main.js` 为占位文件,当前镜像不支持 JS VM
+- 所有业务逻辑(如入群审批、组队邀请)在前端 API 层实现
+
+## 环境配置
+
+前端通过 `.env` 文件配置:
+- `VITE_PB_URL` — PocketBase 地址(默认 `window.location.origin`)
+- `VITE_PORT` — 开发服务器端口
diff --git a/backend/pb_migrations/1776421645_created_groups.js b/backend/pb_migrations/1776421645_created_groups.js
index ec09605..48acb34 100644
--- a/backend/pb_migrations/1776421645_created_groups.js
+++ b/backend/pb_migrations/1776421645_created_groups.js
@@ -87,7 +87,7 @@ migrate((db) => {
"listRule": "@request.auth.id != \"\"",
"viewRule": "@request.auth.id != \"\"",
"createRule": "@request.auth.id != \"\"",
- "updateRule": "owner = @request.auth.id",
+ "updateRule": "@request.auth.id != \"\"",
"deleteRule": "owner = @request.auth.id",
"options": {}
});
diff --git a/backend/pb_migrations/1776440604_updated_groups.js b/backend/pb_migrations/1776440604_updated_groups.js
new file mode 100644
index 0000000..1862cf4
--- /dev/null
+++ b/backend/pb_migrations/1776440604_updated_groups.js
@@ -0,0 +1,18 @@
+///
+migrate((db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf")
+
+ collection.listRule = "@request.auth.id != \"\""
+ collection.viewRule = "@request.auth.id != \"\""
+
+ return dao.saveCollection(collection)
+}, (db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf")
+
+ collection.listRule = "owner = @request.auth.id || members.id = @request.auth.id"
+ collection.viewRule = "owner = @request.auth.id || members.id = @request.auth.id"
+
+ return dao.saveCollection(collection)
+})
diff --git a/backend/pb_migrations/1776443079_updated_groups.js b/backend/pb_migrations/1776443079_updated_groups.js
new file mode 100644
index 0000000..60e8417
--- /dev/null
+++ b/backend/pb_migrations/1776443079_updated_groups.js
@@ -0,0 +1,16 @@
+///
+migrate((db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf")
+
+ collection.updateRule = "@request.auth.id != \"\""
+
+ return dao.saveCollection(collection)
+}, (db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf")
+
+ collection.updateRule = "owner = @request.auth.id"
+
+ return dao.saveCollection(collection)
+})
diff --git a/backend/pb_migrations/1776443393_updated_groups.js b/backend/pb_migrations/1776443393_updated_groups.js
new file mode 100644
index 0000000..e2e10e7
--- /dev/null
+++ b/backend/pb_migrations/1776443393_updated_groups.js
@@ -0,0 +1,27 @@
+///
+migrate((db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf")
+
+ // add
+ collection.schema.addField(new SchemaField({
+ "system": false,
+ "id": "sf_approval",
+ "name": "requireApproval",
+ "type": "bool",
+ "required": false,
+ "presentable": false,
+ "unique": false,
+ "options": {}
+ }))
+
+ return dao.saveCollection(collection)
+}, (db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("es63bkyiblpnxdf")
+
+ // remove
+ collection.schema.removeField("sf_approval")
+
+ return dao.saveCollection(collection)
+})
diff --git a/backend/pb_migrations/1776443448_created_join_requests.js b/backend/pb_migrations/1776443448_created_join_requests.js
new file mode 100644
index 0000000..c75bb4b
--- /dev/null
+++ b/backend/pb_migrations/1776443448_created_join_requests.js
@@ -0,0 +1,90 @@
+///
+migrate((db) => {
+ const collection = new Collection({
+ "id": "ezklls35klxregi",
+ "created": "2026-04-17 16:30:48.222Z",
+ "updated": "2026-04-17 16:30:48.222Z",
+ "name": "join_requests",
+ "type": "base",
+ "system": false,
+ "schema": [
+ {
+ "system": false,
+ "id": "sf_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": "sf_user",
+ "name": "user",
+ "type": "relation",
+ "required": true,
+ "presentable": false,
+ "unique": false,
+ "options": {
+ "collectionId": "_pb_users_auth_",
+ "cascadeDelete": true,
+ "minSelect": null,
+ "maxSelect": 1,
+ "displayFields": null
+ }
+ },
+ {
+ "system": false,
+ "id": "sf_status",
+ "name": "status",
+ "type": "select",
+ "required": true,
+ "presentable": false,
+ "unique": false,
+ "options": {
+ "maxSelect": 1,
+ "values": [
+ "pending",
+ "approved",
+ "rejected"
+ ]
+ }
+ },
+ {
+ "system": false,
+ "id": "sf_reason",
+ "name": "rejectReason",
+ "type": "text",
+ "required": false,
+ "presentable": false,
+ "unique": false,
+ "options": {
+ "min": null,
+ "max": 200,
+ "pattern": ""
+ }
+ }
+ ],
+ "indexes": [],
+ "listRule": "@request.auth.id != \"\"",
+ "viewRule": "@request.auth.id != \"\"",
+ "createRule": "@request.auth.id != \"\"",
+ "updateRule": "group.owner = @request.auth.id",
+ "deleteRule": "group.owner = @request.auth.id",
+ "options": {}
+ });
+
+ return Dao(db).saveCollection(collection);
+}, (db) => {
+ const dao = new Dao(db);
+ const collection = dao.findCollectionByNameOrId("ezklls35klxregi");
+
+ return dao.deleteCollection(collection);
+})
diff --git a/backend/pb_migrations/1776480065_updated_users.js b/backend/pb_migrations/1776480065_updated_users.js
new file mode 100644
index 0000000..a25e7e0
--- /dev/null
+++ b/backend/pb_migrations/1776480065_updated_users.js
@@ -0,0 +1,18 @@
+///
+migrate((db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
+
+ collection.listRule = "@request.auth.id != \"\""
+ collection.viewRule = "@request.auth.id != \"\""
+
+ return dao.saveCollection(collection)
+}, (db) => {
+ const dao = new Dao(db)
+ const collection = dao.findCollectionByNameOrId("_pb_users_auth_")
+
+ collection.listRule = "id = @request.auth.id"
+ collection.viewRule = "id = @request.auth.id"
+
+ return dao.saveCollection(collection)
+})
diff --git a/docs/plans/2026-04-17-game-group-v2-design.md b/docs/plans/2026-04-17-game-group-v2-design.md
index df5c73c..2fdbd29 100644
--- a/docs/plans/2026-04-17-game-group-v2-design.md
+++ b/docs/plans/2026-04-17-game-group-v2-design.md
@@ -159,6 +159,19 @@ NAS 服务器
icon: string
awardedAt: date
}
+
+// memories - 多媒体记忆(音视频等)
+{
+ id: string
+ groupId: string
+ uploader: string (userId)
+ title: string
+ description: string
+ file: string (PocketBase file field)
+ fileType: "image" | "video" | "audio" | "other"
+ size: number (bytes)
+ createdAt: date
+}
```
### 3.3 三期数据集合
@@ -398,11 +411,12 @@ NAS 服务器
### 第二期
-**目标**: 预约 + 积分 + 荣誉
+**目标**: 预约 + 积分 + 荣誉 + 记忆
- [ ] 预约系统(简化投票)
- [ ] 积分系统(获取/记录)
- [ ] 荣誉墙(自动授予)
+- [ ] 多媒体记忆(支持音视频等多媒体文件的上传、下载与在线预览)
### 第三期
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
index c4eb609..0f10985 100644
--- a/frontend/nginx.conf
+++ b/frontend/nginx.conf
@@ -10,6 +10,9 @@ server {
location / {
try_files $uri $uri/ /index.html;
+ add_header Cache-Control "no-cache, no-store, must-revalidate";
+ add_header Pragma "no-cache";
+ add_header Expires "0";
}
# API 代理到局域网 PocketBase
diff --git a/frontend/src/components/team/IdleMembersList.vue b/frontend/src/components/team/IdleMembersList.vue
index a0bcbe0..0359788 100644
--- a/frontend/src/components/team/IdleMembersList.vue
+++ b/frontend/src/components/team/IdleMembersList.vue
@@ -2,6 +2,7 @@