feat: initialize PocketBase backend with migrations

- Created backend directory structure
- Added .env configuration for PocketBase
- Added initial migration with users, groups, games, teamSessions, invitations collections
- Added docker-compose.yml for containerized deployment
- Added README.md with setup instructions

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
congsh
2026-04-17 14:14:05 +08:00
commit 4d1a53fc69
6 changed files with 4794 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
PB_PORT=8090
PB_DATA=./pb_data
PB_MIGRATIONS=./pb_migrations
+16
View File
@@ -0,0 +1,16 @@
# Game Group V2 Backend
PocketBase BaaS 服务
## 启动
```bash
# 方式1: Docker
docker-compose up -d
# 方式2: 直接运行
./pocketbase serve --env .env
```
访问: http://localhost:8090/_/
管理后台: http://localhost:8090/_/ (admin/admin)
+14
View File
@@ -0,0 +1,14 @@
version: '3.8'
services:
pocketbase:
image: ghcr.io/muchobien/pocketbase:latest
container_name: gamegroup-pb
ports:
- "${PB_PORT:-8090}:8090"
volumes:
- ./pb_data:/pb_data
- ./pb_migrations:/pb_migrations
environment:
- GO_ENV=production
restart: unless-stopped
+781
View File
@@ -0,0 +1,781 @@
/// <reference path="../pb_data/types.d.ts" />
migrate((app) => {
// Users collection
app.deleteCollection("users");
app.createCollection("users", {
id: "users",
name: "users",
type: "auth",
fields: [
{
id: "new_aQr3td8dJl",
name: "username",
type: "text",
system: false,
required: true,
unique: true,
options: {
min: 3,
max: 20,
pattern: "^[a-zA-Z0-9_]+$",
}
},
{
id: "new_Jx4zWq9kLm",
name: "displayName",
type: "text",
system: false,
required: true,
unique: false,
options: {
min: null,
max: 50,
pattern: "",
}
},
{
id: "new_Xp7vR2sT4u",
name: "avatar",
type: "url",
system: false,
required: false,
unique: false,
options: {
exceptDomains: null,
onlyDomains: null,
}
},
{
id: "new_Nh8gD3fF6g",
name: "bio",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: 500,
pattern: "",
}
},
{
id: "new_Vc5bH8kJ7o",
name: "region",
type: "select",
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
values: [
"cn",
"us",
"eu",
"asia",
"other"
],
}
},
{
id: "new_Tf6rK9mP8q",
name: "preferences",
type: "json",
system: false,
required: false,
unique: false,
options: {
maxSize: 10000,
}
},
{
id: "new_Dg7hL0nR9s",
name: "stats",
type: "json",
system: false,
required: false,
unique: false,
options: {
maxSize: 10000,
}
},
{
id: "new_Wm8jO1pS0t",
name: "isActive",
type: "bool",
system: false,
required: false,
unique: false,
options: {}
},
{
id: "new_Yn9kQ2qT1u",
name: "lastSeen",
type: "date",
system: false,
required: false,
unique: false,
options: {
min: "",
max: "",
}
},
],
indexes: [
"create index users_username_idx on users (username)",
"create index users_region_idx on users (region)",
"create index users_isActive_idx on users (isActive)",
],
listRule: "id = @request.auth.id",
viewRule: "id = @request.auth.id",
createRule: null,
updateRule: "id = @request.auth.id",
deleteRule: "id = @request.auth.id",
options: {
allowEmailAuth: true,
allowOAuth2Auth: false,
allowUsernameAuth: false,
exceptEmailDomains: null,
manageRule: null,
minPasswordLength: 8,
onlyEmailDomains: null,
requireEmail: false,
}
});
// Games collection
app.deleteCollection("games");
app.createCollection("games", {
id: "games",
name: "games",
type: "base",
fields: [
{
id: "new_Zp3wT8sV5x",
name: "name",
type: "text",
system: false,
required: true,
unique: true,
options: {
min: 1,
max: 100,
pattern: "",
}
},
{
id: "new_Bq4xU9tW6y",
name: "nameEn",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: 100,
pattern: "",
}
},
{
id: "new_Cr5yV0uX7z",
name: "cover",
type: "url",
system: false,
required: false,
unique: false,
options: {
exceptDomains: null,
onlyDomains: null,
}
},
{
id: "new_Ds6zW1vY8a",
name: "description",
type: "editor",
system: false,
required: false,
unique: false,
options: {
convertUrls: false,
}
},
{
id: "new_Et7aX2wZ9b",
name: "type",
type: "select",
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
values: [
"fps",
"moba",
"rpg",
"strategy",
"racing",
"sports",
"casual",
"other"
],
}
},
{
id: "new_Fu8bY3xA0c",
name: "platform",
type: "select",
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
values: [
"pc",
"mobile",
"console",
"crossplay"
],
}
},
{
id: "new_Gv9cZ4yB1d",
name: "maxTeamSize",
type: "number",
system: false,
required: true,
unique: false,
options: {
min: 2,
max: 100,
noDecimal: true,
}
},
{
id: "new_Hw0dA5zC2e",
name: "tags",
type: "json",
system: false,
required: false,
unique: false,
options: {
maxSize: 5000,
}
},
{
id: "new_Ix1eB6aD3f",
name: "isActive",
type: "bool",
system: false,
required: false,
unique: false,
options: {}
},
],
indexes: [
"create index games_name_idx on games (name)",
"create index games_type_idx on games (type)",
"create index games_platform_idx on games (platform)",
"create index games_isActive_idx on games (isActive)",
],
listRule: "",
viewRule: "",
createRule: null,
updateRule: null,
deleteRule: null,
options: {
allowEmailAuth: false,
allowOAuth2Auth: false,
allowUsernameAuth: false,
exceptEmailDomains: null,
manageRule: null,
onlyEmailDomains: null,
requireEmail: false,
}
});
// Groups collection
app.deleteCollection("groups");
app.createCollection("groups", {
id: "groups",
name: "groups",
type: "base",
fields: [
{
id: "new_Jy2fC7bE4g",
name: "name",
type: "text",
system: false,
required: true,
unique: false,
options: {
min: 3,
max: 50,
pattern: "",
}
},
{
id: "new_Kz3gD8cF5h",
name: "description",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: 500,
pattern: "",
}
},
{
id: "new_La4hE9dG6i",
name: "avatar",
type: "url",
system: false,
required: false,
unique: false,
options: {
exceptDomains: null,
onlyDomains: null,
}
},
{
id: "new_Mb5iF0eH7j",
name: "owner",
type: "relation",
system: false,
required: true,
unique: false,
options: {
collectionId: "users",
cascadeDelete: true,
minSelect: null,
maxSelect: 1,
displayFields: [
"username",
"displayName"
],
}
},
{
id: "new_Nc6jG1fI8k",
name: "game",
type: "relation",
system: false,
required: true,
unique: false,
options: {
collectionId: "games",
cascadeDelete: false,
minSelect: null,
maxSelect: 1,
displayFields: [
"name"
],
}
},
{
id: "new_Od7kH2gJ9l",
name: "members",
type: "relation",
system: false,
required: false,
unique: false,
options: {
collectionId: "users",
cascadeDelete: false,
minSelect: null,
maxSelect: null,
displayFields: [
"username",
"displayName"
],
}
},
{
id: "new_Pe8lI3hK0m",
name: "maxMembers",
type: "number",
system: false,
required: true,
unique: false,
options: {
min: 2,
max: 100,
noDecimal: true,
}
},
{
id: "new_Qf9mJ4iL1n",
name: "status",
type: "select",
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
values: [
"recruiting",
"full",
"inactive"
],
}
},
{
id: "new_Rg0nK5jM2o",
name: "tags",
type: "json",
system: false,
required: false,
unique: false,
options: {
maxSize: 5000,
}
},
{
id: "new_Sh1oL6kN3p",
name: "requirements",
type: "json",
system: false,
required: false,
unique: false,
options: {
maxSize: 10000,
}
},
{
id: "new_Ti2pM7lO4q",
name: "stats",
type: "json",
system: false,
required: false,
unique: false,
options: {
maxSize: 10000,
}
},
],
indexes: [
"create index groups_name_idx on groups (name)",
"create index groups_owner_idx on groups (owner)",
"create index groups_game_idx on groups (game)",
"create index groups_status_idx on groups (status)",
],
listRule: "",
viewRule: "",
createRule: "@request.auth.id != \"\"",
updateRule: "owner = @request.auth.id",
deleteRule: "owner = @request.auth.id",
options: {
allowEmailAuth: false,
allowOAuth2Auth: false,
allowUsernameAuth: false,
exceptEmailDomains: null,
manageRule: null,
onlyEmailDomains: null,
requireEmail: false,
}
});
// Team Sessions collection
app.deleteCollection("teamSessions");
app.createCollection("teamSessions", {
id: "teamSessions",
name: "teamSessions",
type: "base",
fields: [
{
id: "new_Uj3qN8mP5r",
name: "group",
type: "relation",
system: false,
required: true,
unique: false,
options: {
collectionId: "groups",
cascadeDelete: true,
minSelect: null,
maxSelect: 1,
displayFields: [
"name"
],
}
},
{
id: "new_Vk4rO9nQ6s",
name: "host",
type: "relation",
system: false,
required: true,
unique: false,
options: {
collectionId: "users",
cascadeDelete: false,
minSelect: null,
maxSelect: 1,
displayFields: [
"username",
"displayName"
],
}
},
{
id: "new_Wl5sP0oR7t",
name: "participants",
type: "relation",
system: false,
required: false,
unique: false,
options: {
collectionId: "users",
cascadeDelete: false,
minSelect: null,
maxSelect: null,
displayFields: [
"username",
"displayName"
],
}
},
{
id: "new_Xm6tQ1pS8u",
name: "status",
type: "select",
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
values: [
"waiting",
"playing",
"completed",
"cancelled"
],
}
},
{
id: "new_Yn7uR2qT9v",
name: "voiceChat",
type: "bool",
system: false,
required: false,
unique: false,
options: {}
},
{
id: "new_Zo8vS3rU0w",
name: "roomInfo",
type: "json",
system: false,
required: false,
unique: false,
options: {
maxSize: 10000,
}
},
{
id: "new_Ap9wT4sV1x",
name: "notes",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: 1000,
pattern: "",
}
},
{
id: "new_Bq0xU5tW2y",
name: "scheduledAt",
type: "date",
system: false,
required: false,
unique: false,
options: {
min: "",
max: "",
}
},
{
id: "new_Cr1yV6uX3z",
name: "startedAt",
type: "date",
system: false,
required: false,
unique: false,
options: {
min: "",
max: "",
}
},
{
id: "new_Ds2zW7vY4a",
name: "endedAt",
type: "date",
system: false,
required: false,
unique: false,
options: {
min: "",
max: "",
}
},
],
indexes: [
"create index teamSessions_group_idx on teamSessions (group)",
"create index teamSessions_host_idx on teamSessions (host)",
"create index teamSessions_status_idx on teamSessions (status)",
"create index teamSessions_scheduledAt_idx on teamSessions (scheduledAt)",
],
listRule: "group.owner = @request.auth.id || group.members.id = @request.auth.id",
viewRule: "group.owner = @request.auth.id || group.members.id = @request.auth.id",
createRule: "@request.auth.id != \"\"",
updateRule: "group.owner = @request.auth.id",
deleteRule: "group.owner = @request.auth.id",
options: {
allowEmailAuth: false,
allowOAuth2Auth: false,
allowUsernameAuth: false,
exceptEmailDomains: null,
manageRule: null,
onlyEmailDomains: null,
requireEmail: false,
}
});
// Invitations collection
app.deleteCollection("invitations");
app.createCollection("invitations", {
id: "invitations",
name: "invitations",
type: "base",
fields: [
{
id: "new_Et3yZ8wB5c",
name: "group",
type: "relation",
system: false,
required: true,
unique: false,
options: {
collectionId: "groups",
cascadeDelete: true,
minSelect: null,
maxSelect: 1,
displayFields: [
"name"
],
}
},
{
id: "new_Fu4zA9xC6d",
name: "sender",
type: "relation",
system: false,
required: true,
unique: false,
options: {
collectionId: "users",
cascadeDelete: false,
minSelect: null,
maxSelect: 1,
displayFields: [
"username",
"displayName"
],
}
},
{
id: "new_Gv5aB0yD7e",
name: "recipient",
type: "relation",
system: false,
required: true,
unique: false,
options: {
collectionId: "users",
cascadeDelete: false,
minSelect: null,
maxSelect: 1,
displayFields: [
"username",
"displayName"
],
}
},
{
id: "new_Hw6bC1zE8f",
name: "status",
type: "select",
system: false,
required: true,
unique: false,
options: {
maxSelect: 1,
values: [
"pending",
"accepted",
"rejected",
"cancelled"
],
}
},
{
id: "new_Ix7cD2aF9g",
name: "message",
type: "text",
system: false,
required: false,
unique: false,
options: {
min: null,
max: 500,
pattern: "",
}
},
{
id: "new_Jy8dE3bG0h",
name: "respondedAt",
type: "date",
system: false,
required: false,
unique: false,
options: {
min: "",
max: "",
}
},
],
indexes: [
"create index invitations_group_idx on invitations (group)",
"create index invitations_sender_idx on invitations (sender)",
"create index invitations_recipient_idx on invitations (recipient)",
"create index invitations_status_idx on invitations (status)",
"create unique index invitations_unique_pending on invitations (group, recipient) where status = 'pending'",
],
listRule: "sender = @request.auth.id || recipient = @request.auth.id || group.owner = @request.auth.id",
viewRule: "sender = @request.auth.id || recipient = @request.auth.id || group.owner = @request.auth.id",
createRule: "group.owner = @request.auth.id",
updateRule: "recipient = @request.auth.id",
deleteRule: "sender = @request.auth.id || group.owner = @request.auth.id",
options: {
allowEmailAuth: false,
allowOAuth2Auth: false,
allowUsernameAuth: false,
exceptEmailDomains: null,
manageRule: null,
onlyEmailDomains: null,
requireEmail: false,
}
});
}, (app) => {
// Rollback
app.deleteCollection("invitations");
app.deleteCollection("teamSessions");
app.deleteCollection("groups");
app.deleteCollection("games");
app.deleteCollection("users");
});
@@ -0,0 +1,569 @@
# Game Group V2 设计文档
**日期**: 2026-04-17
**版本**: v2.0
**目标**: 重构游戏小队约玩系统,简化组队和投票流程,改进多群组隔离性
---
## 一、项目概述
### 1.1 现有问题分析
基于现有 game-group 项目的使用反馈:
- **组队麻烦** - 原有匹配机制难以避免时间对齐问题,朋友之间闹矛盾会被强制匹配到一起
- **投票麻烦** - 投票流程过于复杂
- **隔离性不足** - 只有一个群组,缺乏多群组支持
### 1.2 设计目标
- **握手式匹配** - 类似 Steam 邀请机制,自主选择是否加入
- **多群组支持** - 每人可加入 3-10 个群组(管理员可配置)
- **临时小组** - 组队后创建临时小组,游戏结束后解散
- **轻量级后端** - 使用自建 BaaS 服务(PocketBase),部署在 NAS
---
## 二、技术架构
### 2.1 技术栈
| 层级 | 技术选型 | 说明 |
|------|---------|------|
| 后端 | PocketBase | Go 语言,单文件可执行,内置 SQLite |
| 数据库 | SQLite | 轻量级,适合 NAS 部署 |
| 前端 | Vue 3 + TypeScript | 保持原有技术栈 |
| UI 框架 | Element Plus | 保持原有技术栈 |
| 样式 | Tailwind CSS | 保持原有技术栈 |
| 实时通信 | WebSocket | PocketBase 内置支持 |
### 2.2 部署架构
```
NAS 服务器
├── PocketBase (端口可配置,默认 8090)
│ ├── SQLite 数据库文件
│ └── 上传文件存储
└── 前端静态文件 (可选 NAS Web 服务器)
```
---
## 三、数据模型
### 3.1 核心数据集合
```javascript
// users - 用户
{
id: string
username: string
email: string
avatar: string
status: "idle" | "working" | "in_team" | "away" // 用户状态
statusNote: string // 状态备注/拒绝原因
maxGroups: number // 可加入群组数(默认5
workdays: number[] // 工作日 [1-7]
workStartTime: string // 工作开始时间 "HH:mm"
nextWorkTime: number // 下次工作时间戳
points: number // 积分(二期)
createdAt: date
updatedAt: date
}
// groups - 群组(长期)
{
id: string
name: string
description: string
owner: string (userId)
members: string[] (userIds)
maxMembers: number
honors: array (二期 - 荣誉墙)
createdAt: date
updatedAt: date
}
// teamSessions - 临时小组(短期)
{
id: string
sourceGroup: string (groupId)
name: string
gameName: string
members: string[] (userIds)
status: "recruiting" | "playing" | "finished" | "dissolved"
createdAt: date
dissolvedAt: date
}
// invitations - 邀请记录
{
id: string
from: string (userId)
to: string (userId)
teamSession: string (teamSessionId)
status: "pending" | "accepted" | "rejected"
rejectReason: string
createdAt: date
respondedAt: date
}
// games - 游戏库
{
id: string
name: string
platform: "PC" | "PS5" | "Xbox" | "Switch" | "Mobile"
tags: string[]
cover: string (url)
popularCount: number
createdAt: date
}
```
### 3.2 二期数据集合
```javascript
// appointments - 预约系统
{
id: string
groupId: string
gameName: string
scheduledTime: date
initiator: string (userId)
participants: array
[{ userId, status: "going" | "maybe" | "not_going" }]
note: string
status: "pending" | "confirmed" | "cancelled"
createdAt: date
}
// pointsHistory - 积分记录
{
id: string
userId: string
groupId: string
type: "team_joined" | "game_completed" | "appointment_attended" | "abandoned"
amount: number
reason: string
createdAt: date
}
// honors - 荣誉墙
{
id: string
userId: string
groupId: string
title: string
description: string
icon: string
awardedAt: date
}
```
### 3.3 三期数据集合
```javascript
// ledgers - 账目
{
id: string
groupId: string
type: "income" | "expense" | "settlement"
amount: number
category: string
description: string
createdBy: string (userId)
relatedMembers: string[] (userIds)
settled: boolean
occurredAt: date
createdAt: date
}
// ledgerShares - 分摊明细
{
id: string
ledgerId: string
userId: string
shareAmount: number
paidAmount: number
status: "pending" | "paid" | "waived"
}
// assets - 资产
{
id: string
groupId: string
name: string
type: "game_account" | "console" | "equipment"
credentials: string (加密)
status: "available" | "borrowed" | "maintenance"
currentBorrower: string (userId)
note: string
createdAt: date
}
// borrowLogs - 借还记录
{
id: string
assetId: string
borrower: string (userId)
borrowTime: date
returnTime: date
conditionNote: string
status: "active" | "returned" | "damaged"
}
```
### 3.4 四期数据集合
```javascript
// blacklist - 黑名单
{
id: string
groupId: string
reporter: string (userId)
targetGameId: string
reason: string
category: "behavior" | "cheating" | "abandonment" | "other"
description: string
evidence: string[] (urls)
status: "pending" | "approved" | "rejected"
reviewedBy: string (userId)
reviewedNote: string
createdAt: date
reviewedAt: date
}
// bets - 竞猜
{
id: string
groupId: string
title: string
type: "winner" | "score" | "custom"
options: array
[{ id, name, odds }]
participants: object (userId -> optionId)
stakeAmount: number
status: "open" | "closed" | "settled"
resultOptionId: string
createdBy: string (userId)
closeTime: date
createdAt: date
settledAt: date
}
// betParticipations - 下注记录
{
id: string
betId: string
userId: string
selectedOptionId: string
stakeAmount: number
winAmount: number
status: "pending" | "won" | "lost"
}
```
---
## 四、核心功能设计
### 4.1 用户状态(中文化)
| 状态值 | 显示 | 图标 |
|--------|------|------|
| idle | 空闲 | 🟢 |
| working | 工作中 | 🔴 |
| in_team | 组队中 | 🔵 |
| away | 离开 | ⚫ |
### 4.2 握手匹配流程
```
┌─────────────────────────────────────────────────────────────┐
│ 状态管理与匹配 │
└─────────────────────────────────────────────────────────────┘
1. 用户设置状态
用户 → 点击"我空了" → status: idle
用户 → 设置工作时间 → workSchedule 更新
定时任务 → 检查 nextWorkTime → 自动变更为 working
2. 查看空闲成员
进入群组 → 显示 members 中 status=idle 的用户
实时订阅 → members 状态变更时自动更新列表
3. 发起邀请
选择空闲成员 → 创建 teamSession (status: recruiting)
批量创建 invitations (status: pending)
被邀请者收到实时通知
4. 响应邀请
接受 → invitation.status: accepted, user.status: in_team
加入 teamSession.members
拒绝 → invitation.status: rejected, 填写 rejectReason
发送者看到拒绝原因
5. 组队完成
所有受邀者响应 → teamSession.status: playing
显示临时小组信息
6. 解散小组
任何人点击"游戏结束" → teamSession.status: dissolved
所有成员 status 恢复原状
```
### 4.3 工作时间自动管理
```
用户设置:
- workdays: [1,2,3,4,5] (周一到周五)
- workStartTime: "09:00"
系统计算:
- nextWorkTime = 下一个工作日的 09:00
- 定时检查: 当前时间 >= nextWorkTime ? 自动变更为 working
- 手动改状态后: nextWorkTime = 下一个工作日的 09:00
```
### 4.4 邀请机制
- **一对一邀请** - 逐个发起,可连续邀请
- **拒绝原因隐私** - 只有发起邀请的人能看到拒绝原因
- **实时通知** - 使用 PocketBase 实时订阅推送邀请
---
## 五、前端界面设计
### 5.1 页面结构
```
登录/注册页
主布局
├── 侧边栏
│ ├── 群组列表
│ ├── 工作时间设置
│ └── 个人状态切换
├── 主内容区
│ ├── 当前群组视图
│ │ ├── 空闲成员列表
│ │ │ └── 每个成员: 头像 + 状态 + "邀请"按钮
│ │ ├── 游戏选择
│ │ └── 我的临时小组
│ │ └── 成员状态 + 游戏结束按钮
│ └── 游戏库页面
│ ├── 游戏列表/搜索
│ └── 热门游戏
└── 通知中心
└── 邀请通知 (接受/拒绝)
```
### 5.2 核心交互
1. **状态切换**
- 顶部状态指示器,点击切换
- 工作时间弹窗设置
2. **邀请流程**
- 点击成员旁"邀请" → 选择游戏 → 创建临时小组 → 发送邀请
- 已邀请的成员显示"等待响应"
3. **响应邀请**
- 通知中心弹出邀请卡片
- 显示:发起人、游戏、其他已加入成员
- 按钮:接受 / 拒绝(输入原因)
4. **临时小组**
- 显示所有已加入成员
- 谁都可以点"游戏结束"解散
---
## 六、功能分期
### 第一期(MVP
**目标**: 核心组队功能
- [x] 用户认证(注册/登录)
- [x] 状态管理(空闲/工作中/组队中/离开)
- [x] 工作时间设定
- [x] 群组管理(创建/加入/退出)
- [x] 握手式邀请(发起/接受/拒绝)
- [x] 临时小组(创建/解散)
- [x] 游戏库(列表/搜索/热门)
- [x] 实时状态同步
### 第二期
**目标**: 预约 + 积分 + 荣誉
- [ ] 预约系统(简化投票)
- [ ] 积分系统(获取/记录)
- [ ] 荣誉墙(自动授予)
### 第三期
**目标**: 财务 + 资产
- [ ] 账目管理(记录/分摊/结算)
- [ ] 资产管理(登记/借用/归还)
### 第四期
**目标**: 黑名单 + 竞猜
- [ ] 黑名单系统(举报/审核)
- [ ] 竞猜系统(创建/下注/开奖)
---
## 七、项目文件结构
```
game-group-v2/
├── backend/
│ ├── pocketbase/
│ │ ├── pb_migrations/ # 数据库迁移文件
│ │ ├── pb_data/ # 数据存储(SQLite
│ │ └── pocketbase # 可执行文件
│ ├── .env # 配置(端口等)
│ └── docker-compose.yml # 可选:容器化部署
├── frontend/
│ ├── src/
│ │ ├── api/
│ │ │ ├── pocketbase.ts # PB 实例初始化
│ │ │ ├── users.ts # 用户相关
│ │ │ ├── groups.ts # 群组相关
│ │ │ ├── sessions.ts # 临时小组相关
│ │ │ ├── invitations.ts # 邀请相关
│ │ │ └── games.ts # 游戏库相关
│ │ │ ├── appointments.ts # 预约(二期)
│ │ │ ├── ledgers.ts # 账目(三期)
│ │ │ ├── assets.ts # 资产(三期)
│ │ │ ├── blacklist.ts # 黑名单(四期)
│ │ │ └── bets.ts # 竞猜(四期)
│ │ ├── components/
│ │ │ ├── common/ # 通用组件
│ │ │ ├── layout/ # 布局组件
│ │ │ └── team/ # 组队相关组件
│ │ │ ├── StatusToggle.vue # 状态切换
│ │ │ ├── IdleMembersList.vue # 空闲成员
│ │ │ ├── InviteButton.vue # 邀请按钮
│ │ │ ├── InvitationCard.vue # 邀请卡片
│ │ │ ├── TeamSessionPanel.vue # 临时小组面板
│ │ │ └── WorkScheduleModal.vue # 工作时间设置
│ │ ├── views/
│ │ │ ├── Login.vue
│ │ │ ├── Register.vue
│ │ │ ├── GroupView.vue # 群组主页
│ │ │ └── GamesLibrary.vue # 游戏库
│ │ ├── stores/
│ │ │ ├── user.ts # 用户状态
│ │ │ ├── group.ts # 当前群组
│ │ │ └── team.ts # 当前临时小组
│ │ ├── types/
│ │ │ ├── user.ts
│ │ │ ├── group.ts
│ │ │ ├── session.ts
│ │ │ └── game.ts
│ │ ├── router/
│ │ ├── utils/
│ │ └── App.vue
│ ├── .env # API_URL 等配置
│ ├── package.json
│ └── vite.config.ts
└── docs/
├── design.md # 本设计文档
├── api.md # API 文档
└── migration.md # 迁移指南
```
---
## 八、配置说明
### 8.1 后端环境变量
```env
# .env
PB_PORT=8090 # PocketBase 端口(可配置)
PB_DATA=./pb_data # 数据存储路径
PB_MIGRATIONS=./pb_migrations
```
### 8.2 前端环境变量
```env
# .env
VITE_PB_URL=http://your-nas-ip:8090
```
---
## 九、从原项目迁移
### 9.1 保留的设计
| 功能 | 原实现 | 新实现 |
|------|--------|--------|
| 用户认证 | JWT + NestJS | PocketBase 内置 |
| 权限管理 | 自定义 Guard | PocketBase API Rules |
| 实时通信 | - | PocketBase 实时订阅 |
| 前端框架 | Vue 3 + Element Plus | 保持不变 |
### 9.2 主要改进
| 方面 | 原实现 | 新实现 |
|------|--------|--------|
| 匹配方式 | 自动匹配 | 握手式邀请 |
| 投票 | 复杂投票 | 快速响应(去/可能/不去) |
| 群组 | 单群组 | 多群组 + 临时小组 |
| 数据库 | MySQL | SQLite |
| 后端 | NestJS | PocketBase |
---
## 十、附录
### 10.1 状态码对照表
| 类别 | 值 | 显示 |
|------|-----|------|
| 用户状态 | idle | 空闲 🟢 |
| | working | 工作中 🔴 |
| | in_team | 组队中 🔵 |
| | away | 离开 ⚫ |
| 临时小组 | recruiting | 招募中 |
| | playing | 游戏中 |
| | finished | 已结束 |
| | dissolved | 已解散 |
| 邀请 | pending | 等待响应 |
| | accepted | 已接受 |
| | rejected | 已拒绝 |
### 10.2 积分规则(二期)
| 行为 | 积分 |
|------|------|
| 成功组队 | +10 |
| 完成游戏 | +20 |
| 准时赴约 | +15 |
| 放鸽子 | -30 |
### 10.3 荣誉徽章(二期)
| 条件 | 徽章 |
|------|------|
| 组队100次 | 组队达人 |
| 连续10次赴约 | 全勤玩家 |
| 积分TOP1 | 群组之星 |
---
**文档版本**: v1.0
**更新日期**: 2026-04-17
File diff suppressed because it is too large Load Diff