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:
@@ -0,0 +1,3 @@
|
||||
PB_PORT=8090
|
||||
PB_DATA=./pb_data
|
||||
PB_MIGRATIONS=./pb_migrations
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
Reference in New Issue
Block a user