Клиенты и подписки
Модель
Section titled “Модель”Четыре сущности вместо одной таблицы «пользователь»:
| Сущность | Отвечает на вопрос | Жизненный цикл |
|---|---|---|
| Клиент | Кто это? | Живёт, пока существует человек |
| Подписка | Что и до когда ему доступно? | Активируется, продлевается, истекает |
| План | Условия продажи | Справочник, управляется админом |
| Группа доступа | Какие инбаунды входят? | Справочник |
erDiagram
clients ||--o{ subscriptions : ""
plans ||--o{ subscriptions : "необязательно"
access_groups ||--o{ subscriptions : ""
access_groups ||--o{ access_group_inbounds : ""
inbounds ||--o{ access_group_inbounds : ""
subscriptions ||--o{ protocol_credentials : ""
Разделение обосновано разными жизненными циклами: у клиента может быть история подписок (продления — новые периоды, а не перезапись полей), подписка может существовать без плана (выдана вручную), план меняется без влияния на уже проданные подписки.
Клиент
Section titled “Клиент”Идентичность: email (уникален) и опциональное имя. Появляется двумя путями: создание админом (сразу активен) или саморегистрация, если включена — с режимами открыто / по заявке / закрыто (Саморегистрация). Вход в кабинет — magic link, пароль или OAuth (методы входа).
Статусы: pending (заявка на рассмотрении) · active · rejected · disabled (бан). Вход и покупка доступны только active-клиентам с подтверждённым email. Клиент может существовать без подписок — статус клиента управляет доступом в кабинет, статусы подписок — доступом к VPN; это независимые измерения.
Подписка
Section titled “Подписка”| Поле | Назначение |
|---|---|
client_id |
Владелец |
plan_id |
Nullable: подписка, выданная вручную, плана не имеет |
access_group_id |
Какие инбаунды доступны. При покупке копируется из плана |
status |
active | expired | suspended | disabled |
expires_at |
Nullable — бессрочная |
traffic_limit_bytes |
Nullable — безлимит. Копируется из плана при покупке |
traffic_reset |
none | monthly — сброс расхода по границе периода |
token |
Секрет ссылки подписки (/sub/{token}), 🔒 шифруется |
access_group_id и traffic_limit_bytes копируются из плана в момент активации сознательно: изменение плана задним числом не должно менять условия уже проданных подписок. Это снапшот условий сделки, а не дублирование данных.
Состояния
Section titled “Состояния”stateDiagram-v2
direction LR
[*] --> active
active --> expired : expires_at прошёл
expired --> active : продление
active --> suspended : лимит трафика исчерпан
suspended --> active : сброс периода / продление
active --> disabled : бан админом
disabled --> active : разбан
Только active-подписки попадают в рендер desired state. Любой переход статуса инкрементирует ревизии затронутых нод — отзыв доступа доходит до Xray за секунды при живом стриме.
expired и suspended вычисляются фоновым процессом и обработчиком отчётов о трафике соответственно; disabled ставится только админом и не снимается автоматикой.
Креденшелы
Section titled “Креденшелы”Секреты протоколов генерируются backend-ом per-подписка per-протокол (лениво, при первом появлении протокола в группе доступа):
| Протокол | Секрет |
|---|---|
| VLESS/VMess | UUID |
| Trojan | Случайный пароль (32 байта) |
| Shadowsocks | Случайный пароль |
Хранятся зашифрованными (Шифрование) — они нужны в открытом виде для рендера конфигов нод и ссылки подписки, поэтому шифруются, а не хэшируются.
Перевыпуск ключа (кнопка у клиента и у админа): секреты подписки генерируются заново, ревизии затронутых нод инкрементируются, старые конфиги перестают работать немедленно. Идентификатор подписки при этом не меняется — статистика и история не теряются.
В Xray клиент идентифицируется email-тегом <subscription_id>@astral — счётчики трафика однозначно относятся к подписке без парсинга чего-либо, кроме UUID.
Группы доступа
Section titled “Группы доступа”Группа доступа — именованный набор инбаундов. Единственный механизм выдачи доступа: и планы, и ручные подписки ссылаются на группу. Нет «привязки клиента к ноде», нет особого случая для мультихопа — цепочка задаётся на инбаунде (Мультихоп), подписка этого не видит.
Изменение состава группы немедленно влияет на все её подписки (это операция управления доступом, в отличие от условий плана).
name, price, currency, period, traffic_limit_bytes, traffic_reset, access_group_id. План — шаблон для создания/продления подписок через платежи. Удаление плана не трогает существующие подписки (plan_id → SET NULL — их условия уже скопированы).
Ссылка подписки
Section titled “Ссылка подписки”GET /sub/{token} отдаёт клиентский конфиг: по подписке выбираются все enabled-инбаунды её группы доступа на active-нодах, для каждого рендерится URI (vless://… и т.д.) с адресом ноды и секретом подписки. Формат ответа определяется по User-Agent (Clash, sing-box, v2ray base64) — стандартное поведение для клиентских приложений.