Skip to content

Модель данных БД

Описание схемы PostgreSQL приложения llmgw. Источник истины — SQLAlchemy-модели в backend/app/models/. Миграции — Alembic (backend/alembic/versions/).

Обзор

База хранит:

  • Пользователей и организации — B2B-модель: кошелёк привязан к организации, пользователь может быть участником нескольких организаций.
  • API-ключи — виртуальные ключи LiteLLM с лимитами и статусами.
  • Биллинг — платежи ЮKassa, безналичные пополнения, append-only журнал баланса, бонусы.
  • Прайсинг — правила ценообразования моделей и история курса USD/RUB.
  • Экономика — ручной учёт затрат OpenRouter, налогов и целевой маржи (админка).
  • Инфраструктура — outbox для at-least-once обработки событий.

Все денежные суммы в копейках (BigInteger), кроме полей в USD (Numeric).

Общие соглашения

СоглашениеЗначение
PKUUID v4 в String(36)
Временные меткиcreated_at, updated_atDateTime(timezone=True), UTC
МиксинTimestampMixin в app.models.base
Именование FK/индексовSQLAlchemy naming convention в Base.metadata

ER-диаграмма

mermaid
erDiagram
    users ||--o| organizations : "billing_organization_id"
    users ||--o{ organization_members : "user_id"
    organizations ||--o{ organization_members : "organization_id"
    organizations ||--o{ api_keys : "organization_id"
    users ||--o{ api_keys : "user_id"
    users ||--o{ payments : "user_id"
    users ||--o{ balance_ledger : "user_id"
    organizations ||--o{ balance_ledger : "organization_id"
    users ||--o{ bonuses : "user_id"
    users ||--o{ referrals : "referrer / referred"
    organizations ||--o{ organization_bank_topups : "organization_id"
    balance_ledger ||--o| organization_bank_topups : "ledger_entry_id"
    users ||--o{ openrouter_topups : "created_by"
    users ||--o{ economics_expenses : "created_by"
    users ||--o| economics_settings : "updated_by"

    users {
        uuid id PK
        string email UK
        string password_hash
        string google_sub UK
        string yandex_sub UK
        enum role
        enum status
        uuid billing_organization_id FK
    }

    organizations {
        uuid id PK
        string name
        uuid owner_user_id FK
        string litellm_team_id UK
        bigint balance_kopecks
        bigint total_topped_up_kopecks
    }

    organization_members {
        uuid id PK
        uuid organization_id FK
        uuid user_id FK
        enum role
    }

    api_keys {
        uuid id PK
        uuid user_id FK
        uuid organization_id FK
        string key_hash UK
        enum status
    }

    balance_ledger {
        uuid id PK
        uuid organization_id FK
        uuid user_id FK
        bigint delta_kopecks
        enum reason
        string ref_id
    }

    payments {
        uuid id PK
        uuid user_id FK
        string yookassa_id UK
        enum status
    }

Таблицы

users

Пользователь кабинета. Баланс не хранится на пользователе — он вычисляется через billing_organization.

КолонкаТипОписание
idString(36) PKUUID
emailString(255) UNIQUEЛогин, индекс
password_hashString(255) NULLNULL для OAuth-only аккаунтов
google_subString(255) UNIQUE NULLSubject Google OAuth
yandex_subString(255) UNIQUE NULLSubject Yandex OAuth
roleuser_roleuser | admin
statususer_statusactive | pending_email | blocked
email_verifiedBooleanПодтверждён ли email
email_verified_atDateTime(tz) NULL
billing_organization_idString(36) FK → organizations NULLКошелёк; nullable на первом flush при регистрации
totp_secretString(64) NULL2FA
totp_enabledBoolean
telegram_idBigInteger UNIQUE NULLЗарезервировано
telegram_usernameString(64) NULL
referral_codeString(16) UNIQUE NULLКод для приглашений
referred_byString(36) NULLID пригласившего (без FK)
accepted_offer_atDateTime(tz) NULLСогласие с офертой (152-ФЗ)
accepted_pdn_atDateTime(tz) NULLСогласие на обработку ПДн
accepted_offer_versionString(16) NULLВерсия документа оферты
accepted_pdn_versionString(16) NULLВерсия политики ПДн
accepted_offer_ipString(64) NULLIP при принятии
accepted_user_agentString(512) NULLUser-Agent при принятии
last_login_atDateTime(tz) NULL
notesText NULLАдмин-заметки
created_at, updated_atDateTime(tz)

Связи: api_keys, payments, balance_ledger, bonuses (cascade delete); billing_organizationorganizations.


organizations

Организация (B2B): единый кошелёк, участники, связка с LiteLLM Team.

КолонкаТипОписание
idString(36) PKUUID
nameString(160)Название
owner_user_idString(36) FK → users RESTRICTВладелец
litellm_team_idString(128) UNIQUE NULLID команды в LiteLLM
requisitesText NULLРеквизиты для безнала
balance_kopecksBigIntegerТекущий баланс
total_topped_up_kopecksBigIntegerСумма всех пополнений
created_at, updated_atDateTime(tz)

Связи: organization_members (cascade delete).


organization_members

Членство пользователя в организации. Уникальная пара (organization_id, user_id).

КолонкаТипОписание
idString(36) PK
organization_idString(36) FK CASCADE
user_idString(36) FK CASCADE
roleorg_member_roleowner | admin | member | finance | technical
created_at, updated_atDateTime(tz)

api_keys

Виртуальные ключи доступа к API (проксируются в LiteLLM).

КолонкаТипОписание
idString(36) PK
user_idString(36) FK CASCADEСоздатель
organization_idString(36) FK CASCADEОрганизация-кошелёк
nameString(80)Отображаемое имя
litellm_key_idString(128) UNIQUE NULLID ключа в LiteLLM
masked_keyString(64)Маска вида sk-llmgw-...XXXX
key_hashString(128) UNIQUESHA-256 полного ключа
statusapi_key_statusсм. enum ниже
daily_limit_kopecksBigInteger NULLДневной лимит расхода
monthly_limit_kopecksBigInteger NULLМесячный лимит
rpm_limitInteger NULLRequests per minute
tpm_limitInteger NULLTokens per minute
allowed_modelsJSONСписок моделей; [] = все
last_used_atDateTime(tz) NULL
expires_atDateTime(tz) NULL
created_at, updated_atDateTime(tz)

balance_ledger

Append-only журнал изменений баланса организации. Каждая запись фиксирует delta_kopecks и balance_after_kopecks.

КолонкаТипОписание
idString(36) PK
organization_idString(36) FK CASCADE
user_idString(36) FK CASCADEПользователь-инициатор / владелец операции
delta_kopecksBigInteger+/- к балансу
balance_after_kopecksBigIntegerСнимок баланса после операции
reasonledger_reasonТип операции
ref_idString(128) NULLИдемпотентность: payment_id, LiteLLM request_id, …
descriptionText NULL
extraJSON NULLПроизвольные метаданные
created_at, updated_atDateTime(tz)

Индексы: (user_id, created_at), (organization_id, created_at), (reason, ref_id).


payments

Платежи через ЮKassa.

КолонкаТипОписание
idString(36) PK
user_idString(36) FK CASCADE
yookassa_idString(64) UNIQUE NULLID в ЮKassa
idempotency_keyString(64) UNIQUEКлюч идемпотентности
amount_kopecksBigInteger
currencyString(3)По умолчанию RUB
statuspayment_status
methodString(32) NULLbank_card, sbp, …
descriptionString(255) NULL
confirmation_urlText NULLURL оплаты
receipt_payloadJSON NULLЧек 54-ФЗ
raw_payloadJSON NULLПолный ответ ЮKassa
paid_atDateTime(tz) NULL
refunded_atDateTime(tz) NULL
created_at, updated_atDateTime(tz)

organization_bank_topups

Пополнения по безналу (платёжное поручение), вносятся админом.

КолонкаТипОписание
idString(36) PK
organization_idString(36) FK CASCADE
amount_kopecksBigInteger
payment_dateDateДата платежа
payment_order_numberString(64) UNIQUEНомер платёжного поручения
noteText NULL
ledger_entry_idString(36) FK → balance_ledger SET NULL UNIQUEСвязанная запись журнала
created_by_user_idString(36) FK → users SET NULL
created_at, updated_atDateTime(tz)

bonuses

Начисленные бонусы (триал, welcome, промо, реферал, админ).

КолонкаТипОписание
idString(36) PK
user_idString(36) FK CASCADE
bonus_typebonus_type
amount_kopecksBigInteger
promo_codeString(32) NULL
extraJSON NULL
applied_atDateTime(tz) NULLКогда зачислен на баланс
created_at, updated_atDateTime(tz)

referrals

Реферальная программа (post-MVP placeholder).

КолонкаТипОписание
idString(36) PK
referrer_idString(36) FK CASCADEПригласивший
referred_idString(36) FK CASCADE UNIQUEПриглашённый (один раз)
codeString(16)Использованный код
reward_kopecksBigIntegerНачисленное вознаграждение
created_at, updated_atDateTime(tz)

pricing_rules

Правила ценообразования по модели. PK — имя модели в LiteLLM.

КолонкаТипОписание
model_nameString(128) PKНапример anthropic/claude-sonnet-4.6
display_nameString(128) NULLДля UI
providerString(32) NULLanthropic, openai, …
familyString(32) NULLclaude, gpt, …
base_input_usd_per_mtokNumeric(12,6)База OpenRouter, USD / 1M input
base_output_usd_per_mtokNumeric(12,6)USD / 1M output
base_cached_input_usd_per_mtokNumeric(12,6) NULLКешированный input
markup_pctNumeric(6,2)Наценка платформы, %
context_tokensInteger NULLРазмер контекста
max_output_tokensInteger NULL
featuresJSON NULLvision, tools, …
descriptionText NULL
visibleBooleanПоказывать в каталоге
trial_allowedBooleanДоступна без пополнения
created_at, updated_atDateTime(tz)

Формула цены в ₽:
base_*_usd × (1 + markup_pct/100) × usd_rub × (1 + spread_pct/100).


fx_rates

История курсов USD/RUB (ЦБ + спред).

КолонкаТипОписание
rate_dateDate PKКалендарная дата
usd_rubNumeric(10,4)Курс ЦБ
spread_pctNumeric(5,2)Спред платформы
sourceString(32)По умолчанию cbr
fetched_atDateTime(tz)Время загрузки

openrouter_topups

Ручной учёт пополнений баланса OpenRouter (затраты в USD).

КолонкаТипОписание
idString(36) PK
topup_dateDate
amount_usdNumeric(16,6)
manual_usd_rubNumeric(12,4) NULLРучной курс; иначе ЦБ+спред на дату
noteText NULL
created_by_user_idString(36) FK SET NULL
created_at, updated_atDateTime(tz)

economics_expenses

Ручной учёт налогов и комиссий конвертации.

КолонкаТипОписание
idString(36) PK
expense_dateDate
kindeconomics_expense_kindtax | conversion_fee
amount_kopecksBigInteger
noteText NULL
created_by_user_idString(36) FK SET NULL
created_at, updated_atDateTime(tz)

economics_settings

Singleton настроек экономической аналитики (id = 'default').

КолонкаТипОписание
idString(16) PKФиксированное 'default'
target_margin_pctNumeric(6,2)Целевая маржа, % от выручки
updated_atDateTime(tz)
updated_by_user_idString(36) FK SET NULL

outbox

Transactional outbox для at-least-once доставки (webhook платежей и др.).

КолонкаТипОписание
idString(36) PK
topicString(64)Тип события
ref_idString(128) NULLВнешний идентификатор
payloadJSONТело события
statusoutbox_status
attemptsIntegerЧисло попыток
next_attempt_atDateTime(tz) NULLСледующий retry
last_errorText NULL
processed_atDateTime(tz) NULL
created_at, updated_atDateTime(tz)

Индексы: (status, next_attempt_at), (topic, ref_id).


Перечисления (PostgreSQL ENUM)

user_role

user · admin

user_status

active · pending_email · blocked

org_member_role

owner · admin · member · finance · technical

api_key_status

ЗначениеСмысл
activeРабочий ключ
blockedРучная заморозка / исчерпан дневной лимит
insufficient_fundsАвто-блокировка при balance ≤ 0
deletedУдалён

payment_status

pending · waiting_for_capture · succeeded · canceled · refunded · failed

ledger_reason

ЗначениеСмысл
topupПополнение (ЮKassa / безнал)
usageСписание за usage LiteLLM
bonus_signupТриал при подтверждении email
bonus_topupWelcome-бонус
bonus_referralРеферальный бонус
bonus_promoПромокод
refundВозврат
admin_adjustmentКорректировка админом
chargebackЧарджбэк

bonus_type

email_trial · welcome_topup · referral · promo · admin

outbox_status

pending · processing · done · failed · dead

economics_expense_kind

tax · conversion_fee


Ключевые бизнес-правила

  1. Кошелёк на организации. users.balance_kopecks — вычисляемое свойство через billing_organization. Единственный источник баланса — organizations.balance_kopecks.

  2. Журнал append-only. Любое изменение баланса порождает запись в balance_ledger с balance_after_kopecks. Откат — новая запись с противоположным delta_kopecks.

  3. Идемпотентность. Поле balance_ledger.ref_id предотвращает двойное списание/зачисление (например, один payment_id или LiteLLM request_id).

  4. OAuth. password_hash может быть NULL; идентификация через google_sub / yandex_sub (уникальные).

  5. API-ключи привязаны к организации. Списание usage идёт с баланса organization_id ключа, а не личного кошелька пользователя.

  6. Outbox. Критичные побочные эффекты (зачисление после webhook) пишутся в одной транзакции с основной операцией; воркер обрабатывает outbox с ретраями.


Карта модулей → таблицы

Модуль (backend/app/models/)Таблица
users.pyusers
organizations.pyorganizations, organization_members
api_keys.pyapi_keys
balance_ledger.pybalance_ledger
payments.pypayments
bank_topups.pyorganization_bank_topups
bonuses.pybonuses
referrals.pyreferrals
pricing.pypricing_rules, fx_rates
economics.pyopenrouter_topups, economics_expenses, economics_settings
outbox.pyoutbox

Миграции (хронология)

RevisionФайлОписание
0001_initial20260427_000000_initial_schema.pyБазовая схема
0002_consent_audit20260427_000100_consent_audit_fields.pyАудит согласий 152-ФЗ
0002_orgs20260428_120000_organizations_litellm_teams.pyОрганизации, перенос баланса, LiteLLM teams
0003_org_roles_ft20260429_130000_org_roles_finance_technical.pyРоли finance, technical
0004_economics20260430_120000_economics_analytics.pyЭкономическая аналитика
0005_org_requisites20260501_120000_org_requisites.pyorganizations.requisites
20260519_10000020260519_100000_users_billing_org_nullable_on_create.pyNullable billing_organization_id
20260519_11000020260519_110000_organization_bank_topups.pyБезналичные пополнения
20260519_12000020260519_120000_google_oauth.pygoogle_sub, nullable password_hash
20260519_13000020260519_130000_yandex_oauth.pyyandex_sub

© llmgw