Архитектура MARV
Оглавление
- Обзор архитектуры
- Структура проекта
- Domain-Driven Design
- Слои приложения
- Ключевые паттерны
- Модули системы
- Обработка событий
- Управление конфигурацией
Обзор архитектуры
MARV построен на принципах Domain-Driven Design (DDD) с чётким разделением на слои.
Архитектурные принципы
- Layered Architecture - чёткое разделение ответственности
- Dependency Inversion - зависимости направлены к domain
- Value Objects - типобезопасные идентификаторы
- Event-Driven - асинхронная обработка через EventBus
- Repository Pattern - абстракция доступа к данным
- Factory Pattern - создание сложных объектов
Структура проекта
marv/
├── cmd/ # Точки входа (бинарники)
│ ├── marv/ # Основной сервер
│ │ └── main.go
│ ├── hgoose/ # Утилита миграций БД (goose v3)
│ └── config-migrator/ # Миграция legacy-конфигов
│
├── domain/ # Ядро бизнес-логики (не зависит ни от чего)
│ ├── models/ # Domain модели
│ │ └── vo/ # Value Objects (23 типа ID, с UnmarshalJSON валидацией)
│ ├── services/ # Domain сервисы (чистая логика)
│ ├── repositories/ # Интерфейсы репозиториев
│ ├── interfaces/ # Интерфейсы для внешних зависимостей (cache, config)
│ ├── event_bus/ # Event Bus (sync + async, DLQ, retry)
│ ├── errors/ # Domain ошибки (коды + sentinel)
│ ├── iap/ # Контракты In-App Purchase
│ ├── platform/ # Контракты платформ (VK, OK, TG, FB, ...)
│ └── shared/ # Общие типы (ApiType, ApiTypeRange, RawExpr, stats)
│
├── application/ # Оркестрация use-cases
│ ├── container/ # DI Container (split: container, getters, events, ops)
│ ├── dto/ # Data Transfer Objects
│ ├── mappers/ # Domain ↔ DTO преобразования
│ ├── services/ # Application-сервисы (OnlineTracker и др.)
│ └── interfaces/ # ModuleType (19), CoreModules, SystemModuleSet,
│ # Controller, StatsProvider (source of truth)
│
├── infrastructure/ # Внешние зависимости (PostgreSQL, Redis, HTTP)
│ ├── database/ # PostgreSQL (GORM) + mapper + ORM модели
│ ├── cache/ # Redis + MemoryStore (тесты)
│ ├── config/ # Koanf + JSON Schema валидация
│ ├── logger/ # Zerolog (file + stdout)
│ ├── http/ # HTTP-клиент + retry
│ ├── repositories/ # GORM-реализации репозиториев
│ ├── platform/ # Интеграции платформ
│ │ ├── clients/ # Discord, GalaxyStore, Myket, Bazaar
│ │ └── providers/ # VK, OK, Yandex, FB, TG, Apple, Google
│ ├── bots/ # Telegram и др. боты
│ ├── cloudflare/ # Cloudflare Stream API
│ ├── tasks/ # Cron tasks
│ │ ├── factory.go
│ │ └── implementations/
│ ├── scheduler/ # gocron + taskrunner + RunTracker (in-memory stats)
│ ├── leader/ # Redis-based leader election
│ ├── rival/ # Rival policy engine
│ ├── trafficflow/ # TrafficFlow callback gateway
│ └── observability/ # Метрики, мониторинг
│ ├── metrics/ # Prometheus (cache, controllers, db, http, tasks)
│ └── system/ # HostInfo (platform, kernel, uptime)
│
├── interfaces/ # HTTP-слой (Gin)
│ └── api/
│ ├── apierrors/ # Маппинг доменных ошибок → HTTP
│ ├── common/ # RouterGroup — обёртка над gin с BeforeUse()
│ ├── controllers/ # HTTP контроллеры
│ │ ├── v1/ # API v1 (legacy, POST-based)
│ │ ├── v2/ # API v2 (основной, RESTful)
│ │ ├── acp/ # Admin Control Panel
│ │ ├── ext/ # External webhooks
│ │ └── api/ # Системный API
│ ├── merchants/ # Merchant handlers
│ ├── middlewares/ # Auth, CORS, rate limit, body limit, timeout, metrics
│ └── router/ # Роутинг: v1, v2, acp, ext (SetupRoutes)
│
├── modules/ # Модульная система (19 модулей, config-driven)
│ ├── contracts.go # Типизированные контракты (V1/V2/Acp/Ext Module)
│ ├── factory.go # Factory для создания модулей
│ ├── manager.go # Менеджер жизненного цикла (идемпотентный)
│ ├── types/ # ModuleType (re-export из application/interfaces)
│ └── implementations/ # 19 реализаций модулей
│
├── internal/ # Внутренние зеркальные реализации
│ ├── infrastructure/ # Декораторы, доп. реализации
│ │ ├── http/ # Внутренний HTTP
│ │ ├── platform/ # Внутренние платформы
│ │ ├── repositories/ # Cache-декораторы (decorator pattern)
│ │ ├── rival/ # Rival policy
│ │ ├── scheduler/ # Внутренний scheduler
│ │ ├── tasks/ # Внутренние задачи
│ │ └── trafficflow/ # Внутренний TrafficFlow
│ └── observability/ # Внутренние метрики
│
├── pkg/ # Shared packages
│ ├── engine/ # Engine: boot, gin, lifecycle, prometheus
│ ├── alerts/ # Alert система (Manager, Templates, Providers)
│ │ ├── manager.go # Emit/Render, template cache (sync.RWMutex)
│ │ ├── config.go # Destinations, Templates, Clone()
│ │ ├── event.go # Event → Alert mapping
│ │ ├── template.go # Go text/template renderer
│ │ └── providers/ # Delivery providers
│ │ ├── helm.go # → HELM Alert Center (structured + tag enrichment)
│ │ ├── slack.go # → Slack webhook
│ │ └── telegram.go # → Telegram Bot API
│ ├── logutil/ # SanitizeURL/Headers/Map
│ ├── runtime/ # Shared singletons
│ │ └── httpclient/ # Thread-safe HTTP client (atomic.Value)
│ └── utils/ # Crypto (MD5, SHA1, SHA256, HMAC, SecureCompare), OAuth
│
├── config/ # Конфигурационные файлы
│ ├── config.example.yml # Пример конфигурации
│ ├── .env.example # Пример переменных окружения (MARV__*)
│ ├── bots.example.json # Пример конфигурации ботов
│ └── products.example.json # Пример каталога продуктов
│
├── scripts/ # Вспомогательные скрипты
│ └── deploy.sh # Помощник деплоя (migrate, run, config-migrate)
│
├── migrations/ # SQL-миграции (goose v3)
├── docs/ # Документация (OpenAPI, VitePress, Redocly)
├── tests/ # Интеграционные тесты
│
├── Dockerfile # Multi-stage build (marv + hgoose + config-migrator)
├── docker-compose.yml # Локальная разработка (app + Postgres + Redis)
├── Makefile # Сборка, линт, тесты, бандлы
└── .gitlab-ci.yml # CI/CD (lint → test → security → build/release)Domain-Driven Design
Value Objects
23 типа типобезопасных идентификаторов в domain/models/vo/:
Core Entities:
├── user_id.go - UserId
├── world_id.go - WorldId
├── message_id.go - MessageId
├── transaction_id.go - TransactionId
├── stream_id.go - StreamId
├── stream_input_id.go - StreamInputId
├── event_id.go - EventId
├── event_result_id.go - EventResultId
├── traffic_flow_id.go - TrafficFlowId
├── traffic_flow_entry_id.go - TrafficFlowEntryId
└── rival_id.go - RivalId
System Entities:
├── ab_test_id.go - AbTestId
├── remote_config_id.go - RemoteConfigId
├── cron_task_id.go - CronTaskId
├── alert_id.go - AlertId
├── alert_delivery_log_id.go - AlertDeliveryLogId
├── acp_user_id.go - AcpUserId
└── ad_id.go - AdId
Backup Entities:
├── user_backup_id.go - UserBackupId
├── world_backup_id.go - WorldBackupId
└── user_data_update_id.go - UserDataUpdateIdПример использования:
// Правильно - невозможно перепутать параметры
func CreateMessage(userID UserId, worldID WorldId, eventID EventId)
// Неправильно - легко перепутать
func CreateMessage(userID int64, worldID int64, eventID int64)Domain Abstractions (domain/shared)
RawExpr — абстракция для raw SQL выражений, позволяющая domain-сервисам формировать SQL-выражения (например, counter + 1) без импорта gorm. Infrastructure-слой конвертирует RawExpr в gorm.Expr:
// domain — формирует выражение
updates["execution_count"] = shared.NewExpr("execution_count + ?", 1)
// infrastructure — конвертирует для GORM
if expr, ok := v.(shared.RawExpr); ok {
out[k] = gorm.Expr(expr.SQL, expr.Vars...)
}UpdateMap Pattern (GORM)
Для обновления записей в PostgreSQL все репозитории используют паттерн UpdateMap — явное формирование map[string]any вместо передачи ORM-структуры в GORM.Updates(). Это необходимо, потому что Updates(struct) игнорирует zero-value поля (false, 0, ""):
// internal/infrastructure/database/mapper/cron_task.go
func CronTaskUpdateMap(m *models.CronTask) map[string]any {
return map[string]any{
"enabled": m.Enabled, // false корректно сохранится
"name": m.Name,
"expression": m.Expression,
// ...
}
}
// infrastructure/repositories/postgres/cron_task.go
func (r *repo) Update(ctx context.Context, task *models.CronTask) error {
return r.db.Model(&orm.CronTaskRecord{}).
Where("id = ?", task.ID).
Updates(dbmapper.CronTaskUpdateMap(task)).Error
}Этот паттерн реализован для всех сущностей: CronTask, Ad, Alert, RemoteConfig, Stream, Event, Message, TrafficFlow, AbTest.
Rich Domain Models
Модели содержат бизнес-логику, а не только данные:
// Rich Model
type User struct {
ID UserId
Banned bool
// ...
}
func (u *User) Ban() { u.Banned = true }
func (u *User) Unban() { u.Banned = false }
func (u *User) IsBanned() bool { return u.Banned }
func (u *User) CanCreateWorld() bool { return !u.IsBanned() }Модели с rich behavior:
User- ban/unban, attach to ACP, session managementWorld- IsDefault(), BelongsToUser()Message- IsCompleted(), MarkAsCompleted(), IsExpired()Stream- IsFull(), CanAddInput()Event- IsActive(), IsFinished(), CanAddResult()
Слои приложения
1. Domain Layer (Ядро)
Ответственность: Бизнес-логика, инварианты, правила
Компоненты:
- Models - сущности и value objects
- Services - сложная domain логика (координация entities)
- Repositories - интерфейсы (не реализации!)
- Events - доменные события
- Errors - доменные ошибки
Правила:
- НЕ зависит от других слоёв
- НЕ знает о БД, HTTP, кэше
- Только чистая бизнес-логика
- Только Go стандартная библиотека + domain dependencies
Пример:
// domain/services/user.go
func (s *UserService) BanUser(ctx context.Context, userID UserId, reason string) error {
user, err := s.repo.GetByID(ctx, userID)
if err != nil {
return err
}
// Бизнес-логика в domain
user.Ban()
// Сохранение через интерфейс
return s.repo.Update(ctx, user)
}2. Application Layer (Оркестрация)
Ответственность: Use cases, координация domain объектов
Компоненты:
- Services - оркестрация domain сервисов
- Container - Dependency Injection
- DTOs - передача данных между слоями
- Mappers - преобразование Domain ↔ DTO
Правила:
- Зависит от Domain
- НЕ зависит от Infrastructure/Interfaces
- Оркестрирует несколько domain сервисов
- Управляет транзакциями
Пример:
// application/services/user.go
func (s *UserService) BanUserWithCleanup(ctx context.Context, dto *BanUserDTO) error {
// Оркестрация нескольких domain операций
if err := s.domain.BanUser(ctx, dto.UserID, dto.Reason); err != nil {
return err
}
// Очистка связанных данных
if err := s.domain.InvalidateSessions(ctx, dto.UserID); err != nil {
return err
}
// Публикация события
event := NewUserBannedEvent(dto.UserID, dto.Reason)
return s.eventBus.Publish(ctx, event)
}3. Infrastructure Layer (Реализации)
Ответственность: Внешние зависимости (БД, кэш, HTTP)
Компоненты:
- Repositories - PostgreSQL реализации
- Cache - Redis реализации
- Config - Koanf конфигурация
- Logger - zerolog логирование
- Platform - интеграции с платформами
Правила:
- Реализует интерфейсы из Domain
- Конвертирует Domain ↔ ORM
- Использует внешние библиотеки
- НЕ содержит бизнес-логику
Пример:
// infrastructure/repositories/postgres/user.go
func (r *userRepository) GetByID(ctx context.Context, id UserId) (*models.User, error) {
var record orm.UserRecord
err := r.db.Where("id = ?", id.Int64()).First(&record).Error
if err != nil {
return nil, err
}
// Конвертация ORM → Domain
return mapper.UserDomainFromRecord(&record), nil
}4. Interfaces Layer (API)
Ответственность: HTTP endpoints, CLI
Компоненты:
- Controllers - HTTP обработчики
- Middlewares - аутентификация, CORS, metrics
- Router - маршрутизация (Gin)
Правила:
- Зависит от Application/Domain интерфейсов
- Валидация входных данных
- Преобразование HTTP ↔ DTO
- НЕ содержит бизнес-логику
Пример:
// interfaces/api/controllers/v2/users.go
func (c *UsersController) BanUser(ctx *gin.Context) {
var dto dto.BanUserDTO
if err := ctx.ShouldBindJSON(&dto); err != nil {
ctx.Error(apierrors.ErrInvalidInput.WithCause(err))
return
}
// Делегирование в application layer
if err := c.service.BanUser(ctx.Request.Context(), &dto); err != nil {
ctx.Error(err)
return
}
ctx.JSON(200, gin.H{"success": true})
}Ключевые паттерны
1. Repository Pattern
Абстракция доступа к данным:
// domain/repositories/user.go (интерфейс)
type UserRepository interface {
GetByID(ctx context.Context, id UserId) (*User, error)
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
}
// infrastructure/repositories/postgres/user.go (реализация)
type userRepository struct {
db database.Store
}
// internal/infrastructure/repositories/decorators/user_cached.go (decorator)
type cachedUserRepository struct {
inner repositories.UserRepository
cache interfaces.CacheStore
}2. Factory Pattern
Создание сложных объектов:
// modules/factory.go
type Factory struct {
modules map[appinterfaces.ModuleType]func(*container.Container) appinterfaces.Module
}
func (f *Factory) Create(moduleType appinterfaces.ModuleType, container *container.Container) (appinterfaces.Module, error) {
creator := f.modules[moduleType]
return creator(container), nil
}Используется для:
- Модули (
modules/factory.go) - Tasks (
internal/infrastructure/tasks/factory.go)
3. Dependency Injection
Центральный контейнер:
// application/container/container.go
type Container struct {
config interfaces.ConfigStore
db database.Store
cache interfaces.CacheStore
eventBus interfaces.EventBus
// ... сервисы
}
func (c *Container) Users() interfaces.UserService {
c.once.Do(func() {
c.users = services.NewUserService(...)
})
return c.users
}4. Event-Driven Architecture
EventBus для async операций:
// domain/event_bus/events/user.go
type UserBannedEvent struct {
UserID UserId
Reason string
}
// domain/event_bus/handlers/user.go
type UserEventHandler struct {
repo repositories.UserRepository
}
func (h *UserEventHandler) HandleUserBanned(ctx context.Context, event *events.UserBannedEvent) error {
// Обработка события
log.Info().Msg("User banned")
return nil
}
// Регистрация в container
func (c *Container) setupEventHandlers() {
handler := handlers.NewUserEventHandler(c.Users())
c.eventBus.Subscribe("user.banned", handler.HandleUserBanned)
}Модули системы
Модульная архитектура
19 модулей в modules/implementations/:
| Модуль | Файл | Описание |
|---|---|---|
| User | user.go | Пользователи и профили |
| World | world.go | Игровые миры |
| Transaction | transaction.go | IAP транзакции |
| Message | message.go | Система сообщений |
| Stream | stream.go | Livestream функционал |
| Event | event.go | Игровые события |
| TrafficFlow | traffic_flow.go | Офферная механика |
| Rival | rival.go | Соперники |
| AbTest | abtest.go | A/B тестирование |
| RemoteConfig | remote_config.go | Удалённые конфиги |
| Ads | ads.go | Реклама |
| Alert | alert.go | Система оповещений |
| Product | product.go | Каталог продуктов |
| Merchant | merchant.go | Merchant хендлеры |
| Platform | platform.go | Platform хендлеры |
| Bots | bots.go | Telegram и др. боты |
| CronTask | cron_task.go | Планировщик |
| AcpUser | acp_user.go | ACP пользователи |
| System | system.go | Системные функции |
Загрузка модулей
Модули загружаются из конфига app.modules (рекомендуется) или программно через engine.With...():
app:
modules:
- user
- world
- transaction
- stream
- eventManager.Create() идемпотентен — дублирование безопасно при комбинации конфига и кода.
Core и System модули определены в application/interfaces/interfaces.go (source of truth) и re-export в modules/types/types.go:
CoreModules— модули, загружаемые всегда:system,user,cron_task,merchant,platform.acp_userдобавляется, если включены ACP-маршруты.SystemModuleSet— множество системных модулей (используется при формированииModuleInfoListдля ACP).
Контракты модулей (modules/contracts.go)
Типизированные интерфейсы модулей для каждого API-слоя:
type V1Module interface {
appinterfaces.Module
GetV1Controllers() []func(*v1.Controller) appinterfaces.Controller
}
type V2Module interface { ... }
type AcpModule interface { ... }
type ExtModule interface { ... }Интерфейс модуля
type Module interface {
Type() types.ModuleType
}Модули реализуют дополнительные интерфейсы для предоставления контроллеров различных API-слоёв (GetV1Controllers(), GetV2Controllers(), GetAcpControllers(), GetExtControllers()). Роутеры опрашивают модули через Manager и регистрируют контроллеры.
Фазы:
- Create — Factory создаёт модуль по типу из конфига (
app.modules) или программно - Routes — Router'ы запрашивают контроллеры у модулей и регистрируют HTTP endpoints
Обработка событий
EventBus
Два типа шин:
// Синхронная (блокирующая)
c.eventBus = event_bus.NewInMemoryEventBus()
// Асинхронная (non-blocking, workers)
c.asyncEventBus = event_bus.NewAsyncEventBus(10)События в системе
| Событие | Тип | Описание |
|---|---|---|
IapVerifiedEvent | Sync | IAP транзакция проверена |
TrafficFlowCompletedEvent | Async | Оффер завершён |
UserBannedEvent | Async | Пользователь забанен |
Регистрация обработчика
// application/container/container.go
func (c *Container) setupEventHandlers() {
// IAP handler
iapHandler := handlers.NewIapVerificationHandler(
c.Transactions(),
c.Users(),
)
c.eventBus.Subscribe("iap.verified", iapHandler.Handle)
}Управление конфигурацией
Подробное описание всех параметров конфигурации — см. Конфигурация.
Data Flow
Типичный запрос (создание world):
1. HTTP Request
↓
2. Controller (interfaces/api/controllers/v2/worlds.go)
- Валидация JSON
- Создание DTO
↓
3. Application Service (application/services/world.go)
- Оркестрация
- Вызов domain сервисов
↓
4. Domain Service (domain/services/world.go)
- Бизнес-логика
- Валидация invariants
↓
5. Repository (infrastructure/repositories/postgres/world.go)
- Конвертация Domain → ORM
- SQL запрос
↓
6. Cache Decorator (internal/infrastructure/repositories/decorators/world_cached.go)
- Инвалидация кэша
↓
7. EventBus
- Публикация WorldCreatedEvent
↓
8. Response
- Конвертация Domain → DTO
- HTTP 201 CreatedТестирование
Структура тестов
domain/services/*_test.go - Domain logic unit tests
application/services/*_test.go - Application use cases tests
interfaces/api/controllers/*_test.go - HTTP handlers testsПаттерн: Local Interfaces + Stubs
// interfaces/api/controllers/v2/users_test.go
type stubUserService struct {
getUserResponse *models.User
getUserError error
}
func (s *stubUserService) GetUser(ctx, id) (*models.User, error) {
return s.getUserResponse, s.getUserError
}
func TestUsersController_GetUser(t *testing.T) {
service := &stubUserService{
getUserResponse: &models.User{ID: 1},
}
controller := NewUsersController(service)
// ... тестируем контроллер
}Метрики и мониторинг
Prometheus метрики
POST /acp/system/metricsМетрики:
- HTTP requests (duration, count, errors)
- DB queries (duration, count)
- Cache operations (hits, misses)
- Task executions (duration, success/failure)
Аналитика и мониторинг
POST /acp/system/overviewСводная информация для ACP-дашборда: статистика пользователей (кэшируется задачей stats_snapshot), пользователи онлайн и сессии за день (Redis), состояние кластера, память, Redis INFO, статусы задач.
POST /acp/meТочка входа ACP: текущий ACP-пользователь, привязанные аккаунты, версия сервера, модули, платформы, маршруты.
Structured Logging
// zerolog
log.Info().
Str("user_id", userID.String()).
Str("action", "ban").
Msg("User banned")Alert система
pkg/alerts/ — полноценная система оповещений с шаблонами и маршрутизацией:
| Компонент | Описание |
|---|---|
Manager | Emit (создание alert), Render (шаблонизация), кэширование шаблонов (RWMutex) |
Config | Destinations (куда отправлять), Templates (как форматировать), Clone() |
Event | Структура alert-события (Type, Severity, Title, Message, Tags, Payload) |
MatchesDestination() | Фильтрация по severity и tags |
Providers:
- HelmProvider — структурированные алерты в HELM Alert Center (
/ext/alerts/ingest), авто-обогащение тегами (source:marv,server:ID,version,env) - SlackProvider — webhook
- TelegramProvider — Bot API
Диспетчер: internal/infrastructure/tasks/implementations/alert_dispatcher.go — cron task, маршрутизирует алерты по destinations
Производительность
Cache Strategy
Кэш:
- Redis - distributed
TTL политики:
- User data: 1 hour
- Worlds: 30 minutes
- Products: 24 hours
- Remote configs: 5 minutes
Database Optimization
Индексы:
- Composite indexes для cleanup операций
- Covering indexes для частых запросов
Connection Pooling:
infrastructure:
database:
max_open_conns: 25
max_idle_conns: 5
conn_max_lifetime: 300sБезопасность
Аутентификация
Client API (V2):
- JWT-авторизация (
Authorization: Bearer <JWT>, токен черезPOST /v2/login) - Single session (jti)
Client API (V1, deprecated):
- Per-request платформенная авторизация через
X-Auth-Token,X-Auth-Type,X-Auth-Uid - Single session mode (deprecated): при
app.session.single_mode.enabledна/v1/users/fetchгенерируетсяX-Set-Session-Token; приblocking: true— запросы без актуального токена блокируются
Верификация токенов платформ (V1):
Базовый Platform предоставляет дефолтную реализацию верификации, которую конкретные платформы могут переопределить:
| Метод | Формула | Описание |
|---|---|---|
VerifyApiToken | MD5(apiType + apiUID + secret) | Проверка X-Auth-Token при регистрации (первичный запрос). |
VerifyAuthToken | MD5(secret_apiType_apiUID) | Проверка auth-токена (формат secret_apiType_apiUID через _). |
Где secret = app.security.secret из конфигурации, apiType = числовой ID платформы, apiUID = UID пользователя.
Платформы с собственной верификацией (VK, OK, Yandex, Facebook и др.) переопределяют эти методы. Платформы без переопределения (Amazon, App Store, Google Play, и т.д.) используют базовую реализацию.
ACP:
- Google OAuth (
Authorization: Bearer <Google-OAuth-JWT>)
External webhooks:
- Signature verification
- IP whitelisting (опционально)
Rate Limiting
In-memory token bucket (per-IP), конфигурируется через:
app:
security:
rate_limit:
enabled: true
rps: 100
burst: 200При превышении — HTTP 429. Старые записи IP автоматически очищаются.
Авторизация
Role-based access control:
router.WithControllerRole(access.RoleAdmin)
router.WithControllerRole(access.RoleModerator)Статический анализ
В CI и линтере включены: gosec (уязвимости), bodyclose (утечка HTTP-ресурсов), noctx (пропущенный context). Сканирование зависимостей: govulncheck.
