Skip to content

Обзор API

MARV предоставляет V2 как основную клиентскую API; V1 поддерживается для обратной совместимости. ACP - административный профиль; EXT - внешние вебхуки мерчантов.

Авторизация (V2)

V2 использует session-based авторизацию через JWT.

1. Login — клиент отправляет POST /v2/login с платформенными данными в JSON body:

json
{
  "api_type": 30,
  "api_uid": "12345",
  "api_token": "platform-signed-token",
  "platform_data": "vk_user_id=12345&sign=abc...",
  "client_version": "1.5.0",
  "language": "ru"
}

Поле platform_data — raw JSON с платформ-специфичными данными для верификации. Содержимое зависит от api_type:

Платформаplatform_dataОписание
VK"vk_user_id=123&sign=abc..."URL-encoded launch params (HMAC-SHA256)
OK"session_key"Ключ сессии для MD5-верификации
DR"session_key"Ключ сессии для MD5-верификации
MS Start"base64-payload"Base64-encoded JSON с RSA подписью
GamePush"base64-query"Base64-encoded query params
ДругиеНе требуется (можно не передавать)

Сервер проверяет платформенный токен, создаёт/получает пользователя и возвращает JWT вместе с профилем:

json
{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": 1709136000,
  "user": { "id": 1, "api_uid": "12345", "data": { ... } },
  "needs_portable_anchor": true
}

Объект user содержит полный профиль (с data), что избавляет от отдельного запроса к /v2/users/me при старте сессии. Поле needs_portable_anchor подсказывает клиенту, что у аккаунта ещё нет кросс-девайсного «якоря» (FB/Google/Apple), и его стоит предложить привязать.

2. Запросы — все остальные V2 эндпоинты требуют JWT в заголовке:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Платформенные заголовки (X-Auth-Token, X-Auth-Type, X-Auth-Uid) для V2 не нужны. Без валидного Bearer-токена запрос будет отклонён с 401.

3. Single session — каждый вызов /v2/login выдаёт новый токен с уникальным jti (JWT ID) и сохраняет его на сервере. Предыдущий токен того же пользователя автоматически инвалидируется: запросы со старым jti получат 401. Один пользователь = одна активная сессия.

4. Истечение — токен действует 24ч (настраивается через app.security.jwt.ttl). При истечении сервер вернёт 401; клиент должен повторить login.

JWT подписан HMAC-SHA256 ключом app.security.jwt.secret. Этот ключ можно использовать в других сервисах для верификации токена. Payload содержит: uid (user_id), pid (public_id; добавляется только если задан), atp (api_type), aui (api_uid), jti (session id), exp, iat.

Авторизация (V1)

V1 использует per-request платформенную авторизацию через заголовки:

ЗаголовокОбязателенОписание
X-Auth-TokenдаТокен авторизации: по умолчанию MD5(secret_apiType_apiUID)
X-Auth-TypeдаТип платформы (целое), выбирает конфигурацию платформы
X-Auth-UidдаUID пользователя на платформе
X-Session-TokenнетСессионный токен (только при single_mode.enabled)
X-Client-VersionнетВерсия клиента; валидируется согласно конфигу
X-Client-Language-CodeнетКод языка клиента; сохраняется в профиле пользователя
X-SessionнетСессия платформы (используется некоторыми платформами для верификации)
X-Access-TokenнетAccess-токен платформы
X-Inviter-UidнетUID пригласившего пользователя (для реферальной системы)

Single session mode (deprecated): при app.session.single_mode.enabled: true и немобильной платформе, при первом вызове /v1/users/fetch без X-Session-Token сервер генерирует токен, сохраняет его в БД и возвращает в заголовке X-Set-Session-Token. Клиент должен передавать этот токен в последующих запросах через X-Session-Token. При blocking: true запросы с устаревшим или отсутствующим токеном отклоняются (код 8 или 11).

Request ID и страна клиента

  • Клиент может (но не обязан) передавать X-Request-Id. Если заголовок не задан, MARV сгенерирует UUID самостоятельно.
  • Каждый ответ содержит X-Request-Id — его можно использовать для сопоставления с логами (в zerolog контекст пишется поле request_id; trace_id — это его алиас в gin-контексте).
  • При работе через Cloudflare заголовок CF-IPCountry проксируется в ответ как X-Client-Country-Code, поэтому страна клиента доступна даже без отдельного API.
  • В ответ также добавляется X-Server-Id: это идентификатор узла (app.server.id из конфига либо автогенерированное значение), что упрощает отладку и мониторинг лидерства.

Верификация IAP

  • ApiType/ApiUid всегда берутся из аутентифицированного пользователя; значения в теле запроса игнорируются.
  • Поле user_id опционально (передаётся внутри store_payload). Если его нет, MARV подставляет ApiUid текущего пользователя. Указывайте user_id явно только на платформах, где IAP использует другой идентификатор (например, Discord).
  • V2 передаёт store-specific данные через store_payload (JSON object). V1 использует flat-поля для обратной совместимости.
  • Необязательное поле merchant_profile позволяет выбрать профиль мерчанта (по умолчанию default).

Пример V2 запроса POST /v2/transactions/iap/verify:

json
{
  "product_id": 10,
  "price": 9.99,
  "currency": "USD",
  "store_payload": {
    "transaction_id": "receipt-123",
    "purchase_token": "purchase-token-abc",
    "package_name": "com.example.app"
  }
}

Ответ включает success, order_id, product_sku, transaction_id (внутренний ID), duplicate (true если транзакция уже существовала) и error (строка причины при неуспешной верификации; отсутствует при успехе).

Поля store_payload по платформам

Все поля ниже передаются внутри store_payload. Основные поля запроса (product_id, price, currency) обязательны для всех платформ.

Платформаpackage_nameproduct_skupurchase_tokenreceipttransaction_iduser_id
Google Play
App Store (iOS)
RuStore
Amazon
Galaxy Store
Bazaar
Myket
Discordопц.
Facebook
Jestопц.
Yandex

✅ — обязательное; опц. — опциональное; — — не используется.

Yandex

IAP-верификация на платформе Yandex не поддерживается. Запрос вернёт ошибку.

Примеры store_payload по платформам

Google Play / RuStore / Bazaar / Myket:

json
{
  "package_name": "com.example.game",
  "product_sku": "gem_pack_100",
  "purchase_token": "opaque-purchase-token"
}

App Store (iOS):

json
{
  "receipt": "MIIT...base64-encoded-receipt-data",
  "transaction_id": "1000000123456789"
}

Amazon:

json
{
  "receipt": "receipt-id-from-amazon",
  "user_id": "amazon-user-id"
}

Galaxy Store:

json
{
  "purchase_token": "payment-id-token"
}

Discord:

json
{
  "transaction_id": "entitlement-id",
  "user_id": "discord-user-id"
}

Facebook:

json
{
  "receipt": "signature.base64payload"
}

Jest:

json
{
  "purchase_token": "jwt-signed-token",
  "transaction_id": "optional-purchase-id"
}

Форматы ответов

  • Успех (V2): { "time": <unix>, "data": <payload> }
  • Ошибка (V2/ACP): { "time": <unix>, "error": { "code": "...", "message": "...", "details": { ... }}} — HTTP-статус только в статус-строке ответа, в теле его нет

Подробно про ошибки и соответствия доменных ошибок см. в V2 Errors.

Конфигурация и таймауты

  • HTTP‑таймауты и бакеты метрик настраиваются в config/main.md, секциях app.http.timeouts.* и observability.metrics.http_buckets_ms.

EXT (мерчанты)

  • Общие принципы и требования описаны в EXT Guide. Для каждого мерчанта используется собственная авторизация/подпись.

ACP: инициализация и мониторинг

  • POST /acp/me — основная точка входа ACP. Возвращает текущего оператора (email из HELM-токена), привязанные аккаунты, информацию о сервере (версия, аптайм, ОС), список включённых модулей, сконфигурированных платформ и активных маршрутов.
  • POST /acp/system/overview — сводная аналитика для дашборда: статистика пользователей (total, today, week, month, DAU, MAU), количество онлайн, сессий за сегодня, состояние кластера (leader), память, Redis и статусы задач.
  • POST /acp/system/metrics — Prometheus-совместимые метрики (HTTP, DB, cache, controllers, tasks). Описание полей и настройки — в Observability.

Реклама (ACP → Ads)

  • /acp/ads/list возвращает все объявления и runtime‑снимок контроллерных метрик (количество вызовов, ошибки, p95) по префиксу /acp/ads/*. Эндпоинт read-only, отдельная роль не требуется.
  • /acp/ads/create|update|delete|toggle требуют роль AccessAds (256). После модификаций ответы всегда содержат commit: ["ads"] и актуальный список ads, чтобы UI мог обновить состояние без повторного запроса.
  • DTO и поля объявлений описаны в openapi.yaml (AcpAd, AcpCreateAdDTO, ...). Поля title/description/data — произвольные JSON‑структуры, weight задаёт приоритет показа.

Alert Center (ACP)

  • /acp/alerts/list — фильтрует критические уведомления (type, severity, status, tags, диапазон дат). Требует роль AccessAlerts (512).
  • /acp/alerts/detail — возвращает карточку уведомления и историю доставок (Slack/Telegram webhooks).
  • /acp/alerts/ack — подтверждает обработку, сохраняет комментарий (виден всем операторам).
  • Конфигурация каналов и шаблонов описана в config/main.md; подробный контракт — в openapi.yaml.

Реклама (V1 → Ads)

  • POST /v1/ads/fetch возвращает доступные объявления (только включённые и отфильтрованные по весу). Дополнительно формируется video_url по domain.stream.subdomain из конфигурации.
  • POST /v1/ads/impression и POST /v1/ads/click инкрементируют счётчики просмотров/кликов по sid. Эти вызовы не возвращают payload.
  • Все V1 Ads маршруты используют стандартную платформенную авторизацию (X-Auth-*). См. структуры V1Ad, V1AdImpressionDTO, V1AdClickDTO в openapi.yaml.

Реклама (V2 → Ads)

  • POST /v2/ads возвращает массив объявлений (V2Ad) с теми же полями, что и V1 (включая вычисленный video_url).
  • POST /v2/ads/impression и POST /v2/ads/click принимают DTO (V2TrackImpression, V2TrackClick) и увеличивают счётчики по sid.
  • Все вызовы требуют V2 авторизацию (Authorization: Bearer JWT). Ответы следуют формату { "time": ..., "data": ... }. Схемы перечислены в openapi.yaml.

Заказы (Orders)

Модуль order реализует маппинг «SKU клиента → транзакция сервера» для IAP-покупок. Клиент создаёт заказ до инициации платежа на платформе, затем резолвит его при получении подтверждения.

Таблица orders

ПолеТипОписание
idserial PK
api_typeintegerТип платформы
api_uidtextUID пользователя на платформе
skutextИдентификатор продукта
transaction_idtext, nullableID транзакции (заполняется при resolve)
statustextpending / completed
created_attimestampВремя создания
updated_attimestampВремя последнего обновления

V1 API

  • POST /v1/orders/create — создаёт pending-заказ. Body: { "sku": "gem_pack_100" }. Возвращает { "order_id": 42, "sku": "gem_pack_100", "status": "pending" }.
  • POST /v1/orders/resolve — привязывает transaction_id к существующему заказу. Body: { "sku": "gem_pack_100", "transaction_id": "txn_abc123" }. Идемпотентен: если заказ с таким sku+transaction_id уже есть — вернёт его.

V2 API

  • POST /v2/orders/create — аналог V1, V2 формат ответа ({ "time": ..., "data": ... }).
  • POST /v2/orders/resolve — аналог V1, V2 формат ответа.

Cron-задача order_cleanup

Удаляет неразрешённые (pending) заказы старше domain.orders.ttl_days (по умолчанию 7 дней). Настройка:

yaml
infrastructure:
  scheduler:
    tasks:
      - name: Order Cleanup
        tag: order_cleanup
        type: order_cleanup
        expression: "0 3 * * *"   # каждый день в 3:00
        enabled: true

Конфигурация

КлючПо умолчаниюОписание
domain.orders.ttl_days7Сколько дней хранить pending-заказы

Кланы (Clans)

Кросс-платформенные кланы (идентичность игрока = api_type + api_uid; clan_tag глобально уникален). Бой клиент-детерминированный; сервер владеет подсчётом, наградами, состоянием и подбором.

V2 (игрок)

  • POST /v2/clans/{create,update,check,get,search,my,leave,join,join-request,request/list,request/mine,request/resolve,member/role,member/kick,transfer-lead,snapshot/update,leaderboard,leader/snapshot} — управление кланом и ростером. get/search/leaderboard/leader/snapshot публичны; остальное требует авторизацию игрока.
  • update — правка настроек клана лидером (announcement, description, join_policy, min_epoch_gate); частичное обновление (меняются лишь переданные не-null поля), только лидер.
  • check — лёгкий поллинг изменений: по секциям members/requests/war отдаёт метку changed_at (unix, для сравнения с last-seen) + count. Метки бампаются только на структурных изменениях (join/leave/kick/роль/лидерство; создание/резолв заявки), НЕ на снапшотах — клиент грузит тяжёлые данные лишь по изменившейся секции. Секция war дополнительно несёт phase_index, a_points/b_points текущей фазы (live-прогресс без обращения к /phases) и a_winning_points/b_winning_points (агрегат за выигранные фазы).
  • request/mine — собственные заявки игрока (куда подал + статус). leader/snapshot — снапшот лидера клана для превью потенциальным вступающим, не загружая весь ростер.
  • Снапшот лоадаута — first-class поле: create/join/join-request принимают {power, schema_version, hash, snapshot} (= snapshot/update одним запросом), плюс отдельный snapshot/update (гейтинг: хеш + rate-limit). Лидер видит снапшот кандидата в request/list; на accept он переносится в членство. Envelope: power/schema_version — колонки, snapshot — JSON-блоб клиента.
  • Отображаемая идентичность (nickname/avatar) обновляется через snapshot/update независимо от гейтинга снапшота: пишется только при реальном изменении, поэтому смена ника/аватара не «съедается» неизменившимся или rate-limit'нутым снапшотом.
  • total_power клана поддерживается сервером: пересчитывается как SUM(power участников) в той же транзакции при изменении состава/силы (create/join/leave/kick + апдейт power из снапшота); показывается в поиске и my. Война же берёт a_power/b_power отдельным свежим SUM на матчмейкинге.
  • Ошибки унифицированы: стабильные code (clan.tag_taken, clan.already_in_clan, clan.full, clan.request_limit, …) с корректными статусами (409/422); unique_violation больше НЕ 500. Полный список — в Кодах ошибок.

Войны кланов (V2)

  • Эндпоинты: POST /v2/clans/war/{current,config,contributions,phases,points/report,rewards,rewards/claim,battle/roster,history,attack/start,attack/result,final,final/result}.
  • Война между двумя кланами — конфигурируемая упорядоченная последовательность фаз (domain.clan_war.phases), каждая длится свою duration. Планировщик закрывает фазу по истечении её окна (сравнивает a_points/b_pointswinner → начисляет winning_points → фиксирует MVP) и активирует следующую, либо финиширует войну. Победитель войны — по сумме winning_points за выигранные фазы; ничья → без победителя. Типы фаз:
    • points — игроки репортят очки (points/report, валиден только на активной points-фазе); очки питают тали фазы, MVP клана и суммарный вклад игрока.
    • battle — 1×1 хендшейк: attack/start (валиден только в активной battle-фазе; проверяет eligibility — обе стороны в топ-N по power, равные ростеры, цель ещё не побеждена, остаток атак — и замораживает снапшоты + сид) → attack/result (исход win/loss без звёзд; победа банкует points_per_win в тали клана). Каждого врага бьют один раз за фазу. battle/roster отдаёт оба боевых отряда ({attacks_cap, clan_a, clan_b}, боец = {api_type, api_uid, status, attacks_used, by?}): statusfree|active|beaten выводится из clan_war_pvp (pending → active, победа → beaten); active — эксклюзивный лок: attack/start по не-free цели отклоняется (clan_war.battle_in_progress) без траты атаки. Ник/power клиент берёт из clans/get.
    • results — неконкурентная пауза (без победителя).
    • squad5x5 — бой 5×5: final отдаёт составы + seed активной фазы (финал лениво создаётся), final/result фиксирует победителя и пишет исход в тали фазы, чтобы общий финализатор присудил winning_points.
  • current отдаёт текущую войну + личный вклад; phases — разбивку по фазам; history — прошлые войны клана.
  • current дополнительно несёт транзиентные поля: current_a_points/current_b_points (тали активной фазы — клиент дёргает phases только когда сдвинулся current_phase_index) и a_members/b_members (маркеры {changed_at, count} ростеров обоих кланов — переподтягивать вражеский ростер лишь при изменении; ростер нужен для отображения MVP фаз). Участник войны (ClanWarMember) несёт avatar, замороженный на матче (без живого join к clan_members).
  • config — разовый запрос при «война доступна»: весь статический конфиг наград (common пер-тир win/lose + индивидуальная лестница + min_points_for_win + delivery) и tier_thresholds (E:0 … S+++:50). Из конфига, без БД — показывает награды для любого тира даже при доставке через почту, где rewards несёт только собственные начисления аккаунта.
  • contributions — матрица вклада член×фаза по обоим кланам текущей войны (для окна статистики по дням): {phase_count, clan_a, clan_b}, где у каждого участника nickname, avatar и массив phases (очки на каждую фазу, 0 где нет вклада). Источник — таблица clan_war_phase_contributions, которую ведёт движок фаз.
  • Тир: у клана есть счётчик tier_points (растёт за победы в войне), маппящийся на тир по порогам (domain.clan_war.tier). Тир замораживается на войне (a_tier/b_tier) и влияет на размер наград.
  • Определение исхода и results-фаза: исход войны (winner_clan_id + тир-очки) фиксируется в момент, когда никакая фаза уже не может изменить победителя — т.е. закрылась последняя соревновательная фаза и активировалась терминальная results-фаза (флаг clan_wars.outcome_finalized). Война при этом остаётся active на весь duration results-фазы (чтобы war/current продолжал её отдавать), с уже заполненным winner_clan_id и current_phase_type: "results". Когда окно results-фазы истекает — война просто → status=finished (повторно исход/тир/награды НЕ начисляются). Если войны без results-фазы — исход фиксируется на закрытии последней фазы (= завершение войны), как раньше.
  • Награды начисляются, как только исход определён (на старте results-фазы, а не в её конце): общая пер-тир (win/lose; win — только тем, кто набрал больше min_points_for_win; ничья → lose обоим) плюс индивидуальная лестница из стадий по суммарным очкам игрока. ReconcileWarRewards берёт войны по outcome_finalized AND NOT rewards_granted. Начисление идемпотентно и переживает сбои (reconcile в планировщике). Все награды участника за войну агрегируются в одно письмо (одна ledger-запись kind="war" на игрока): Award — список всех payload'ов (пер-тир + стадии), Description{ code: "clan_war_result", params: { outcome, opponent_clan_name } } для i18n на клиенте (текст письма рендерит клиент, не сервер). Два режима доставки (rewards.delivery): mail — сервер кладёт письмо в почту; claim — сервер записывает право, клиент забирает через rewards (список любого статуса) + rewards/claim (атомарно pending → claimed, возвращает забранные, повторный вызов пуст).
  • Очки за всё время: вместе с reconcile наград (т.е. как только исход определён, на старте results-фазы) вклад каждого участника (clan_war_members.points) роллапится в all-time clan_members.points_total (кредитуется членству, которое воевало — по замороженной identity; вышедший из клана не кредитуется) и в клановый clans.points_total (сумма по всем участникам). Последний ранжирует клановый лидерборд (clans/leaderboard → сортировка по points_total DESC). Роллап ровно один раз — атомарно с захватом флага rewards_granted в одной транзакции, поэтому ретрай reconcile / конкурентный прогон ничего не задваивают. Так clans/my → members.points_total = вклад аккаунта за всё время (за войну — war/current → my_contribution.points, за фазу — war/contributions).
  • Роспуск клана в войне → форфейт: если воюющий клан распускается (последний участник вышел, или ACP DisbandClan), война не осиротевает — она сразу завершается форфейтом: выживший клан авто-побеждает (winner_clan_id = он, outcome_finalized, status=finished, +win tier points), reconcile выдаёт ему заработанные награды (win-тир + индив. лестница), а распущенному — ничего (его клан удалён, grantClanRewards пропускает несуществующий клан). Идемпотентно (гейт outcome_finalized).
  • Cron: clan_war_matchmaker открывает войны на старте цикла (материализует фазы из расписания); clan_war_scheduler продвигает фазы, делает reconcile наград и просрочивает зависшие атаки.
  • Config (domain.clan_war, авторитетный пример — config/config.example.yml): participation_mode, min_members, power_band_pct, attacks_per_player, retention/retention_wars, attack_reserve_timeout/attack_reserve_on_expire, phases[] ({type, duration, winning_points, points_per_win?, squad_size?}), tier ({points_per_win, points_per_loss, thresholds}), rewards ({delivery, min_points_for_win, common, individual}).

ACP (оператор)

  • Просмотр открыт всем операторам: POST /acp/clans/{list,get}.
  • Мутации требуют роль AccessClan (2048) и пишут аудит: POST /acp/clans/{rename,disband,member/kick,shadow-ban,audit/list}.
  • Ручные триггеры войны (дублируют кроны clan_war_*): POST /acp/clans/war/{run-matchmaking,advance,next-phase}. advance продвигает только фазы, чьё окно уже истекло (как крон, по одному шагу на войну); next-phaseпринудительно закрывает текущую фазу СЕЙЧАС по текущему счёту, активирует следующую и перезапускает таймлайн оставшихся фаз от now (каждая на свой duration), либо завершает войну на последней фазе (ops/тест).
  • Полные DTO и схемы — в openapi.yaml (теги v2.clans, v2.clan_wars, acp.clans, acp.clan_wars).

A/B тесты: Platform-Aware Distribution

При включённой опции platform_aware (per-test boolean в таблице experiments), распределение пользователей по экспериментальных группам учитывает api_type. Это гарантирует, что пропорции групп соблюдаются внутри каждой платформы, а не по всей таблице.

В MARV2 (legacy) используется конфиг-флаг experiments.platform_aware_distribution, в MARV (DDD) — колонка platform_aware на каждом тесте.

Rival Query: TABLESAMPLE

Запрос соперников (/v1/rivals/fetch) использует 3-фазный подход для оптимизации:

  1. TABLESAMPLE SYSTEM_ROWS — блочная выборка O(1), самый быстрый путь
  2. TABLESAMPLE BERNOULLI(1%) — fallback при разреженных данных
  3. ORDER BY RANDOM() — last resort для маленьких таблиц

Это заменяет прежний ORDER BY RANDOM(), который на таблицах с миллионами записей вызывал полный SeqScan + сортировку.

Требование: расширение tsm_system_rows должно быть установлено: CREATE EXTENSION IF NOT EXISTS tsm_system_rows; (миграция включена).

QueryBuilder: Raw SQL

QueryBuilder поддерживает raw SQL через методы Raw() и Scan():

go
recs := []orm.RivalRecord{}
err := db.Query(ctx).
    Raw("SELECT * FROM rivals TABLESAMPLE SYSTEM_ROWS(?) WHERE api_type IN (?) LIMIT ?", 200, apiTypes, limit).
    Scan(&recs).Error

Эти методы доступны через все обёртки: GORM-реализация, metrics-декоратор, transaction.