Skip to content

Структура бэкенда

backend/
├── cmd/
│ └── server/
│ └── main.go # Точка входа: конфиг, DI, запуск
├── internal/
│ ├── config/
│ │ └── config.go # Viper: читает ENV + config.yaml
│ │
│ ├── domain/ # Бизнес-логика (чистая, без зависимостей)
│ │ ├── user/
│ │ │ ├── model.go # User struct
│ │ │ ├── repository.go # Interface репозитория
│ │ │ └── service.go # Бизнес-правила
│ │ ├── subscription/
│ │ ├── node/
│ │ ├── plan/
│ │ └── traffic/
│ │
│ ├── repository/ # Реализации репозиториев (GORM/pgx)
│ │ ├── postgres/
│ │ │ ├── user.go
│ │ │ ├── subscription.go
│ │ │ ├── node.go
│ │ │ └── traffic.go
│ │ └── redis/
│ │ └── session.go
│ │
│ ├── handler/ # HTTP handlers (Fiber)
│ │ ├── middleware/
│ │ │ ├── auth.go # JWT проверка
│ │ │ ├── admin.go # Проверка роли
│ │ │ └── ratelimit.go
│ │ ├── auth.go
│ │ ├── user.go
│ │ ├── subscription.go
│ │ ├── node.go
│ │ ├── plan.go
│ │ ├── stats.go
│ │ ├── sub_link.go # GET /sub/:token
│ │ └── ws.go # WebSocket метрики
│ │
│ ├── router/
│ │ └── router.go # Все маршруты + middleware
│ │
│ ├── worker/ # Asynq задачи
│ │ ├── client.go # Enqueue задач
│ │ ├── server.go # Worker сервер
│ │ ├── sync_user.go # Синхронизация юзера на ноды
│ │ ├── sync_node.go # Синхронизация всех юзеров на ноду
│ │ └── check_expired.go # Крон: проверка истёкших подписок
│ │
│ ├── xray/
│ │ ├── client.go # gRPC клиент для агента на ноде
│ │ ├── sync.go # Логика добавления/удаления клиента
│ │ └── stats.go # Получение статистики трафика
│ │
│ └── platform/
│ ├── database/
│ │ └── postgres.go # Инициализация пула соединений
│ ├── cache/
│ │ └── redis.go
│ └── logger/
│ └── zap.go
├── migrations/ # SQL-файлы (Goose, embedded в бинарник)
│ ├── 00001_init.sql
│ ├── 00002_subscriptions.sql
│ └── ... # Goose-формат: -- +goose Up / -- +goose Down
├── proto/ # .proto файлы для gRPC
│ └── node_agent/
│ └── agent.proto
├── config.yaml # Дефолтный конфиг
├── docker-compose.yml
├── Dockerfile
└── go.mod
POST /api/v1/auth/register
POST /api/v1/auth/login
POST /api/v1/auth/refresh
DELETE /api/v1/auth/logout
GET /api/v1/me
PATCH /api/v1/me
GET /api/v1/me/subscription
GET /api/v1/me/subscription/link
POST /api/v1/me/subscription/link # Создать новую ссылку
GET /api/v1/me/traffic # Статистика трафика
GET /sub/:token # Отдаёт конфиг для VPN-клиентов
# Пользователи
GET /api/v1/admin/users
GET /api/v1/admin/users/:id
PATCH /api/v1/admin/users/:id
DELETE /api/v1/admin/users/:id
POST /api/v1/admin/users/:id/ban
# Подписки
POST /api/v1/admin/users/:id/subscription
PATCH /api/v1/admin/subscriptions/:id
# Ноды
GET /api/v1/admin/nodes
POST /api/v1/admin/nodes
GET /api/v1/admin/nodes/:id
PATCH /api/v1/admin/nodes/:id
DELETE /api/v1/admin/nodes/:id
POST /api/v1/admin/nodes/:id/sync # Ресинх всех юзеров на ноду
# Тарифы
GET /api/v1/admin/plans
POST /api/v1/admin/plans
PATCH /api/v1/admin/plans/:id
# Статистика
GET /api/v1/admin/stats/overview # Всего юзеров, трафик, активные подписки
GET /api/v1/admin/stats/traffic # Трафик по нодам за период
# WebSocket
WS /api/v1/admin/ws/stats # Realtime метрики нод

Зависимости (go.mod ключевые)

Section titled “Зависимости (go.mod ключевые)”
github.com/gofiber/fiber/v3
github.com/golang-jwt/jwt/v5
github.com/jackc/pgx/v5
gorm.io/gorm
gorm.io/driver/postgres
github.com/redis/go-redis/v9
github.com/hibiken/asynq
google.golang.org/grpc
github.com/spf13/viper
go.uber.org/zap
github.com/pressly/goose/v3
github.com/go-playground/validator/v10
golang.org/x/crypto # bcrypt

Миграции компилируются прямо в бинарник — не нужно тащить SQL-файлы рядом с бинарником в продакшне.

internal/platform/database/migrations.go
//go:embed ../../../migrations/*.sql
var embedMigrations embed.FS
func RunMigrations(db *sql.DB) error {
goose.SetBaseFS(embedMigrations)
goose.SetDialect("postgres")
return goose.Up(db, "migrations")
}

Формат файла миграции:

-- migrations/00001_init.sql
-- +goose Up
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
...
);
-- +goose Down
DROP TABLE users;

Запуск при старте сервера — до того как принимать HTTP-запросы:

cmd/server/main.go
db := database.Connect(cfg.Database.URL)
if err := database.RunMigrations(db); err != nil {
log.Fatal("migration failed", zap.Error(err))
}
server:
port: 8080
jwt_secret: ""
access_token_ttl: 15m
refresh_token_ttl: 720h
database:
url: "postgres://user:pass@localhost:5432/astral"
max_connections: 25
redis:
url: "redis://localhost:6379"
xray:
sync_timeout: 10s
cors:
allowed_origins:
- "https://your-domain.com"

Все ошибки возвращаются в едином формате:

{
"code": "SUBSCRIPTION_EXPIRED",
"message": "Срок подписки истёк",
"details": {}
}

Fiber глобальный error handler конвертирует доменные ошибки в HTTP-коды.