Implement billing functionality and enhance role management
CI / Backend (build + test) (push) Successful in 1m22s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s

- Introduced billing capabilities, allowing users to request payments for subscription periods (3/6/12 months) with admin approval via Telegram.
- Updated role management to include a `BillingEnabled` property, preventing billing for admin roles.
- Enhanced the `CreateRoleCommand` and `UpdateRoleCommand` to accept billing parameters, ensuring proper handling during role creation and updates.
- Added new endpoints for billing management and integrated billing checks into VPN config creation to enforce payment requirements.
- Updated related services, models, and tests to support the new billing features, ensuring comprehensive coverage and functionality.
- Enhanced documentation to reflect the new billing processes and role management changes.
This commit is contained in:
Leonid Pershin
2026-07-19 01:38:16 +03:00
parent b980dc6cef
commit b2ae358250
106 changed files with 6018 additions and 66 deletions
+30 -4
View File
@@ -147,6 +147,19 @@ status, createdAt }`. `expiresAt` всегда `null` (лимиты по сро
| GET | `/api/activation/status` | user | — | `{ isActivated, pendingRequest: { id, comment, createdAt } \| null }` |
| POST | `/api/activation/request` | user | `{ comment? }` | `{ id, comment, createdAt }` |
## Billing (пользователь)
Группа `/api/billing`, `RequireAuthorization()` + `IRequiresActivation`. Доступна независимо от роли —
`GET /status` сам сообщает `billingEnabled=false`, если биллинг для роли пользователя не включён.
| Метод | Путь | Тело запроса | Тело ответа |
| ----- | -------------------------------------------- | ---------------------- | ------------- |
| GET | `/api/billing/status` | — | `BillingStatusDto { billingEnabled, paidUntil, suspended, requisitesText, activeRequest: PaymentRequestDto \| null }` |
| POST | `/api/billing/requests` | `{ period }` (`Quarter`/`HalfYear`/`Year`) | `PaymentRequestDto` (`409 Billing.ActiveRequestExists`, если уже есть активная заявка; `409 Billing.UnlimitedRoleNotSupported` для ролей с `MaxConfigs=-1`; `500 Billing.PricingNotConfigured`, если ставка для периода не задана) |
| POST | `/api/billing/requests/{id}/cancel` | — | `204 No Content` (только из `AwaitingPayment`) |
| POST | `/api/billing/requests/{id}/mark-paid` | — | `204 No Content` (`AwaitingPayment → AwaitingConfirmation`, уведомляет админов в Telegram) |
| POST | `/api/billing/requests/{id}/send-requisites` | — | `204 No Content` (дублирует реквизиты в свой Telegram; `409 Telegram.NotLinked`, если Telegram не привязан) |
## Support (пользователь)
Группа `/api/support`, `RequireAuthorization()` + `IRequiresActivation` (кроме `GET /attachments/{id}`,
@@ -245,8 +258,8 @@ reject/approve владением тикета не ограничены. Еди
| POST | `/api/admin/activation-requests/{id}/approve` | admin | — | `204 No Content` |
| POST | `/api/admin/activation-requests/{id}/reject` | admin | `{ reason? }` | `204 No Content` |
| GET | `/api/admin/roles` | admin | — | `RoleDto[]` |
| POST | `/api/admin/roles` | admin | `{ name, maxConfigs, maxIpLimit }` | `RoleDto` |
| PUT | `/api/admin/roles/{id}` | admin | `{ maxConfigs, maxIpLimit }` | `RoleDto` |
| POST | `/api/admin/roles` | admin | `{ name, maxConfigs, maxIpLimit, billingEnabled }` | `RoleDto` (`400`, если `billingEnabled=true` для `name="admin"`) |
| PUT | `/api/admin/roles/{id}` | admin | `{ maxConfigs, maxIpLimit, billingEnabled }` | `RoleDto` (то же ограничение на `admin`; включение `billingEnabled` ретроактивно выдаёт грейс-период уже назначенным пользователям без `PaidUntil`) |
| DELETE | `/api/admin/roles/{id}` | admin | — | `204 No Content` (системные `admin`/`user` удалить нельзя) |
| PATCH | `/api/admin/users/{id}/role` | admin | `{ roleId }` | `204 No Content` (`409 Roles.CannotRemoveLastAdmin`, если у цели сейчас `admin`, новая роль другая, и это единственный админ) |
| GET | `/api/admin/pricing` | admin | — | `PricingSettingsDto` (глобальная справочная цена за конфиг **в месяц**, одна на весь сервис — не per-роль) |
@@ -255,6 +268,19 @@ reject/approve владением тикета не ограничены. Еди
Нет отдельного эндпоинта «активировать напрямую без запроса» — активация только через
approve/reject над `ActivationRequest`.
## Admin — Billing
| Метод | Путь | Роль | Тело запроса | Тело ответа |
| ----- | -------------------------------------------- | ----- | ---------------------------- | ------------- |
| GET | `/api/admin/billing/settings` | admin | — | `BillingSettingsDto { requisitesText, graceDays }` |
| PUT | `/api/admin/billing/settings` | admin | `{ requisitesText, graceDays }` | `BillingSettingsDto` |
| GET | `/api/admin/billing/requests` | admin | query: `status?, page=1, pageSize=20` | `PagedList<AdminPaymentRequestDto>` (включает `userName`) |
| POST | `/api/admin/billing/requests/{id}/confirm` | admin | — | `204 No Content` (продлевает `BillingPaidUntil`, возвращает приостановленные конфиги в `Active`) |
| POST | `/api/admin/billing/requests/{id}/reject` | admin | `{ reason? }` | `204 No Content` |
То же подтверждение/отклонение доступно **из Telegram, не заходя на сайт** — инлайн-кнопки на
уведомлении о заявке (`pay:approve:{id}`/`pay:reject:{id}`, см. [telegram-bot.md](telegram-bot.md)).
## Admin — Nodes
| Метод | Путь | Роль | Тело запроса | Тело ответа |
@@ -341,9 +367,9 @@ totalConfigs, activeConfigs, totalUsedUpBytes, totalUsedDownBytes }` — счи
| --- | -------------------------------------------------------------------- |
| 400 | Ошибка валидации (FluentValidation, не на все команды — см. [backend-conventions.md](backend-conventions.md)) |
| 401 | Нет/просрочен/невалиден access-токен |
| 403 | Нет прав по роли, либо `Auth.NotActivated` |
| 403 | Нет прав по роли, либо `Auth.NotActivated`, либо `Configs.BillingRequired` (просрочена оплата) |
| 404 | Ресурс не найден |
| 409 | Конфликт домена: `Configs.QuotaExceeded`, дубликат имени пользователя при регистрации, уже есть `Pending`-запрос активации, `Support.RoleRequestAlreadyPending`, `Support.TicketClosed` |
| 409 | Конфликт домена: `Configs.QuotaExceeded`, дубликат имени пользователя при регистрации, уже есть `Pending`-запрос активации, `Support.RoleRequestAlreadyPending`, `Support.TicketClosed`, `Billing.ActiveRequestExists`, `Billing.UnlimitedRoleNotSupported`, `Telegram.NotLinked` |
| 422 | Прочие управляемые ошибки, не подошедшие под коды выше |
| 429 | Rate limit (`/api/auth/*`, `/api/auth/telegram/*`, `/sub/{token}`) |
| 500 | Необработанное исключение (перехватывается `UseExceptionHandler()`, тело без деталей) |
+6 -2
View File
@@ -93,8 +93,8 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
(хранение/ротация/отзыв refresh-токенов), `RoleService`, `DbInitializer` (сидинг).
- **3x-ui интеграция**: `XuiPanelGateway : IXuiPanelGateway` поверх `ThreeXui.Net`; кэш клиентов per-node
внутри самого гейтвея (см. ниже — отдельного класса-фабрики нет).
- **Background jobs**: `TrafficSyncService`, `NodeHealthCheckService`, `TrafficRetentionService`
(`BackgroundService` + `PeriodicTimer`).
- **Background jobs**: `TrafficSyncService`, `NodeHealthCheckService`, `TrafficRetentionService`,
`BillingService` (`BackgroundService` + `PeriodicTimer`).
- **Secrets**: `DataProtectionSecretProtector : ISecretProtector` (шифрование паролей нод at-rest,
ASP.NET Core Data Protection, key-ring на томе `dp_keys`).
- **Telegram**: `TelegramNotifier : ITelegramNotifier` — отправка DM-уведомлений через `ITelegramBotClient`.
@@ -221,6 +221,10 @@ POST /api/configs
- **NodeHealthCheckService** — health-probe нод (`IXuiPanelGateway.ProbeAsync`), обновляет `NodeStatus`,
шлёт `nodeStatusChanged` группе `admins`.
- **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика).
- **BillingService** — раз в час обходит пользователей с billing-ролью (`AppRole.BillingEnabled`):
гасит конфиги при просрочке оплаты (`VpnConfig.Suspend()`), шлёт предупреждение за 3 дня до
истечения. Пропускает пользователей с `PaymentRequest` в `AwaitingConfirmation` — конфиги не
гасятся, пока админ не подтвердит/отклонит заявку (см. [domain-model.md](domain-model.md#billing--подписка-по-сроку)).
- Реализованы как обычные `BackgroundService` + `PeriodicTimer`, без внешнего джоб-раннера
(см. [tech-stack.md](tech-stack.md)).
+97 -9
View File
@@ -4,11 +4,14 @@
`AppUser`/`AppRole` — часть Identity (живут в `Infrastructure`, т.к. расширяют `IdentityUser<Guid>`/
`IdentityRole<Guid>`); чистый `PnvPanel.Domain` ссылается на пользователя/роль только по `Guid`.
Тарифы `Plan` и лимиты трафика на конфиг (`TrafficLimit`) не реализованы — единственная квота:
число активных конфигов на роль (`AppRole.MaxConfigs`). Есть глобальная справочная цена за один
конфиг (`PricingSettings`; редактирует только `admin`, но справочно видна и активированным пользователям
в заявке на роль) — это не биллинг: без статусов оплаты, дат окончания
и интеграций с платёжными системами, см. ниже.
Лимиты трафика на конфиг (`TrafficLimit`) не реализованы — квота на число активных конфигов —
только через `AppRole.MaxConfigs`. Есть глобальная справочная цена за один конфиг (`PricingSettings`;
редактирует только `admin`, но справочно видна и активированным пользователям в заявке на роль) —
используется и биллингом (см. ниже) для расчёта суммы заявки на оплату.
**Биллинг (подписка по сроку) реализован, но опционален и включается per-роль**
(`AppRole.BillingEnabled`, недоступен для `admin`) — см. [Billing](#billing--подписка-по-сроку).
Роль без флага живёт как раньше, без ограничений по сроку.
## Диаграмма связей
@@ -105,7 +108,7 @@ AppUser
| `Protocol` | `VpnProtocol` | Денормализовано с inbound |
| `UsedUpBytes` | `long` | Синхронизируется из 3x-ui (только для отображения — лимит трафика не применяется) |
| `UsedDownBytes` | `long` | Синхронизируется из 3x-ui |
| `ExpiresAt` | `DateTimeOffset?`| Зарезервировано, сейчас ничего его не выставляет — конфиг живёт бессрочно |
| `ExpiresAt` | `DateTimeOffset?`| Для billing-ролей — денормализованный `AppUser.BillingPaidUntil` (см. Billing); иначе `null`, конфиг живёт бессрочно. Уходит в `Subscription-Userinfo` для VPN-клиента |
| `Status` | `ConfigStatus` | `Active` / `Disabled` / `Expired` / `LimitReached` / `Revoked`|
| `SubscriptionToken`| `string` | Секрет для публичного `/sub/{token}` |
| `LastSyncAt` | `DateTimeOffset?`| |
@@ -122,6 +125,9 @@ AppUser
удаляет старого, генерирует новый `SubscriptionToken`; квоту **не тратит**. Для случая утечки ссылки.
- `Disable()`/`Enable()` → меняют только статус записи (`Active ↔ Disabled`); отключение/включение
самого клиента в 3x-ui делает хендлер отдельным вызовом гейтвея (используется при блокировке юзера).
- `Suspend()`/`Resume()` → меняют статус (`Active ↔ Expired`), отдельно от `Disable()`/`Enable()`
приостановка за неуплату (биллинг) не должна конфликтовать с блокировкой админом: разблокировка
возвращает в `Active` только то, что было погашено именно блокировкой, и наоборот (см. Billing).
- `Rename(label)` → юзер меняет метку (синкается в 3x-ui как имя клиента).
- Лимит одновременных IP (`limitIp` в 3x-ui) выставляется при создании клиента (`Create`/`Rotate`) по
квоте роли пользователя (`AppRole.MaxIpLimit`; -1 = без лимита) — панель не даёт настраивать его
@@ -138,9 +144,9 @@ AppUser
- Инбаунд должен быть доступен роли пользователя (`Inbound.AllowedRoles`).
- Разрешено несколько конфигов в одном инбаунде (ограничение — только общая квота роли).
> Лимиты трафика и автоматическое истечение срока конфига не реализованы. `ExpiresAt` никогда не
> выставляется; `ConfigStatus.LimitReached` в значении enum есть, но код в него никогда не переводит
> конфиг. Квота на число конфигов реализована через `AppRole.MaxConfigs` (см. [tech-stack.md](tech-stack.md)).
> Лимиты трафика не реализованы. `ConfigStatus.LimitReached` в значении enum есть, но код в него
> никогда не переводит конфиг. Квота на число конфигов реализована через `AppRole.MaxConfigs` (см.
> [tech-stack.md](tech-stack.md)). Истечение срока — только для billing-ролей, см. Billing ниже.
### TrafficSample — история трафика (для графиков)
Точки потребления во времени; пишутся синхронизацией.
@@ -290,6 +296,7 @@ UI **настойчиво напоминает** привязать его (ед
| `MaxConfigs` | `int` | Квота активных конфигов (-1 = без лимита; для `admin` — без лимита) |
| `MaxIpLimit` | `int` | Лимит одновременных IP на клиента (`limitIp` в 3x-ui; -1 = без лимита; для `admin` — без лимита) |
| `IsSystem` | `bool` | Системная (`admin`, `user`) — нельзя удалить/переименовать |
| `BillingEnabled` | `bool` | Включает биллинг для пользователей с этой ролью; нельзя включить для `admin` (см. Billing) |
Сидируются: `admin` (оба лимита без ограничения) и `user` (`MaxConfigs` = `Roles__DefaultUserMaxConfigs`,
по умолчанию 3; `MaxIpLimit` = `Roles__DefaultUserMaxIpLimit`, по умолчанию 2).
@@ -348,6 +355,87 @@ PricePerConfigPerQuarter × 3`).
admin- и user-facing путями безопасно. Сидируется пустой строкой при старте (`IPricingSettingsSeeder`,
если таблица пуста) и заново после полного сброса панели (см. «Полный сброс панели» выше).
### Billing — подписка по сроку
Опциональная подсистема: включается per-роль (`AppRole.BillingEnabled`), недоступна для `admin`.
Роль без флага не затрагивается — конфиги живут бессрочно, как без биллинга вообще.
**`AppUser` (доп. поля, только для billing-ролей):**
| Поле | Тип | Заметки |
| --------------------------------- | ------------------ | ------------------------------------------------------------ |
| `BillingPaidUntil` | `DateTimeOffset?` | Оплачено до этой даты; `null` — оплата ещё ни разу не выставлялась |
| `BillingSuspended` | `bool` | Конфиги приостановлены за неуплату (см. `BillingService`) |
| `BillingLastWarnedForPaidUntil` | `DateTimeOffset?` | Для какого `PaidUntil` уже отправлено предупреждение «истекает через N дней» — не даёт слать повторно на каждый тик джобы |
**Грейс-период**: когда пользователю впервые назначается billing-роль (или роли, где он уже состоит,
включают `BillingEnabled`) и `BillingPaidUntil == null``RoleService` выставляет
`BillingPaidUntil = now + BillingSettings.GraceDays` автоматически (`ChangeUserRoleAsync`/
`UpdateRoleAsync`). Без этого пользователь был бы «просрочен» с первой секунды.
### BillingSettings — глобальные настройки биллинга
Singleton (как `PricingSettings`) — реквизиты для оплаты и длина грейс-периода, редактирует `admin`.
| Поле | Тип | Заметки |
| ----------------- | ---------------- | ----------------------------------------------------------- |
| `Id` | `Guid` | PK |
| `RequisitesText` | `string` | Произвольный текст реквизитов (карта/крипто-адрес/СБП и т.д.), показывается пользователю с заявкой |
| `GraceDays` | `int` | По умолчанию 7 (`BillingSettings.DefaultGraceDays`) |
| `UpdatedAt` | `DateTimeOffset` | |
### PaymentRequest — заявка на оплату
Пользователь оформляет заявку на период (3/6/12 мес); решает админ на сайте или в Telegram. Не более
одной активной (`AwaitingPayment`/`AwaitingConfirmation`) заявки на пользователя — инвариант
проверяется в `CreatePaymentRequestCommandHandler`.
| Поле | Тип | Заметки |
| ------------------ | ----------------------- | ------------------------------------------------------------ |
| `Id` | `Guid` | PK |
| `UserId` | `Guid` | FK → AppUser (заявитель) |
| `Period` | `PaymentPeriod` | `Quarter` (3 мес) / `HalfYear` (6 мес) / `Year` (12 мес) |
| `AmountSnapshot` | `int` | Сумма, замороженная на момент создания: `ставка PricingSettings за период × MaxConfigs роли × число месяцев`. Последующее изменение прайса админом не меняет уже созданные заявки |
| `Status` | `PaymentRequestStatus` | `AwaitingPayment``AwaitingConfirmation``Confirmed`/`Rejected`, либо `Cancelled` из `AwaitingPayment` |
| `DecidedBy`/`DecidedAt`/`RejectionReason` | | Кто/когда решил, причина отказа (опционально) |
| `CreatedAt` | `DateTimeOffset` | |
Роль с `MaxConfigs = -1` (unlimited) не поддерживает биллинг по формуле —
`CreatePaymentRequestCommandHandler` отдаёт `BillingErrors.UnlimitedRoleNotSupported`.
**Переходы** (`backend/src/PnvPanel.Domain/Billing/PaymentRequest.cs`):
- `Create(userId, period, amount)``AwaitingPayment`, показываются реквизиты `BillingSettings`.
Пользователь может `Cancel()` (только из `AwaitingPayment`) или дождаться проверки.
- `MarkPaymentSent()` → пользователь нажал «Я оплатил»; `AwaitingPayment → AwaitingConfirmation`,
админам уходит Telegram-уведомление с инлайн-кнопками `pay:approve:{id}`/`pay:reject:{id}`.
- `Confirm(adminId)`/`Reject(adminId, reason)` → допустимы из **обоих** `AwaitingPayment` и
`AwaitingConfirmation` (админ мог заметить оплату раньше, чем пользователь нажал кнопку).
`Confirm` продлевает `AppUser.BillingPaidUntil = max(текущий, сейчас) + период` (не теряет уже
оплаченный остаток при досрочной оплате), возвращает в `Active` конфиги, приостановленные за
неуплату (`Suspend()`/`Resume()` на `VpnConfig`, статус `Expired`), обновляет `ExpiresAt` на всех
конфигах пользователя.
### BillingService — приостановка за неуплату (фоновая джоба)
`Infrastructure/BackgroundJobs/BillingService.cs`, раз в час (по образцу `TrafficSyncService`). Для
каждого пользователя с billing-ролью, не заблокированного (`IsBlocked`):
- есть `PaymentRequest` в статусе `AwaitingConfirmation`**пропустить** — это и есть защита «заявка
висит на подтверждении админом, а срок истёк» из требований: конфиги не гасятся, пока админ не
решит (не по вине пользователя, что админ не успел проверить оплату);
- `BillingPaidUntil` в прошлом (или `null`) и ещё не `BillingSuspended` → приостановить все `Active`
конфиги (`Suspend()``Expired`, гейтвей `UpdateClientAsync(enable:false)`, идемпотентно как в
`BlockUserCommandHandler`), `AppUser.BillingSuspended = true`, Telegram-уведомление пользователю,
`AuditLog` (`BillingSuspended`, источник `System`). На последующих тиках (уже suspended) — только
идемпотентная досуспензия «зависших» `Active`-конфигов (самовосстановление после недоступности
ноды), без повторных уведомлений;
- до истечения ≤ 3 дней и предупреждение для этого `PaidUntil` ещё не отправлено
(`BillingLastWarnedForPaidUntil != PaidUntil`) → Telegram-предупреждение, отметка отправки.
`CreateVpnConfigCommandHandler` дополнительно не даёт создать **новый** конфиг, если роль billing
и оплата просрочена (`ConfigErrors.BillingRequired`) — иначе приостановку можно было бы обойти
созданием свежего конфига.
`GET/POST /api/billing/*` — пользователь (статус, создание/отмена заявки, «я оплатил», отправка
реквизитов в свой Telegram). `GET/PUT/POST /api/admin/billing/*` — админ (настройки, список заявок,
подтверждение/отклонение), только `admin`.
### ActivationRequest — запрос активации
Пользователь просит активацию у админа; админ одобряет/отклоняет на сайте или в Telegram.
+2 -1
View File
@@ -75,7 +75,8 @@
| -------------------------- | --------------------------------------------------------------------------------- |
| Ролей у пользователя | Ровно одна роль (квота = `MaxConfigs` роли) |
| Секреты нод | ASP.NET Core Data Protection (шифрование at-rest, key-ring на томе) |
| Тарифы/лимиты трафика | Не реализованы — конфиги без лимитов трафика/срока. Есть глобальная справочная цена за конфиг (`PricingSettings`, редактирует только `admin`, справочно видна и в заявке на роль) — без биллинг-логики |
| Лимиты трафика | Не реализованы — конфиги без лимитов трафика. Есть глобальная справочная цена за конфиг (`PricingSettings`, редактирует только `admin`) — ставка ₽/конфиг/месяц по периодам 3/6/12 мес |
| Биллинг (подписка по сроку) | Реализован, но **включается per-роль** (`AppRole.BillingEnabled`, недоступен для `admin`) — см. [domain-model.md](domain-model.md#billing--подписка-по-сроку). Не биллинг-система по умолчанию: роль без флага живёт без ограничений по сроку, как раньше |
| i18n | RU + EN (react-i18next) |
| Telegram-транспорт | Long polling |
| Регистрация через Telegram | Поддержана (логин — Telegram `@username`/id, пароль генерируется и присылается в чат) |
+25
View File
@@ -197,6 +197,30 @@ Telegram ──updates──► TelegramBotHostedService → PnvBotUpdateHandl
4. Пользователю (если Telegram привязан) — DM «✅ Ваша заявка на роль одобрена.» / «❌ Ваша заявка на
роль отклонена.».
## Флоу 7 — Оплата подписки (биллинг)
Только для ролей с `AppRole.BillingEnabled` (см. [domain-model.md](domain-model.md#billing--подписка-по-сроку)).
Заявка (`PaymentRequest`) заводится и решается частично на сайте, частично в Telegram — симметрично
заявке на роль:
1. Пользователь создаёт заявку и жмёт «Я оплатил» на сайте (`/billing`) — бот в это не вовлечён,
кроме опциональной кнопки «Отправить реквизиты в Telegram» (DM самому себе для удобства, статус
заявки не меняет).
2. `MarkPaymentSentCommandHandler` вызывает
`ITelegramNotifier.NotifyAdminsPaymentRequestedAsync(requestId, userName, period, amount, ct)`.
3. Сообщение с кнопками **«✅ Подтвердить» / «❌ Отклонить»** (callback `pay:approve:{id}`/`pay:reject:{id}`
— тот же 3-частный формат, что и `rrq:*`/`act:*`).
4. Нажатие → проверка прав (`TrySetAdminCurrentUserAsync`) → `ConfirmPaymentRequestCommand`/
`RejectPaymentRequestCommand` (те же команды, что дёргает `POST /api/admin/billing/requests/{id}/confirm|reject`
на сайте). Подтверждение продлевает `AppUser.BillingPaidUntil` и возвращает приостановленные
конфиги в `Active`. Исходное сообщение редактируется (дописывается статус), как у `rrq:*`/`act:*`.
5. Пользователю (если Telegram привязан) — DM «✅ Оплата подтверждена. Доступ продлён до {дата}.» /
«❌ Заявка на оплату отклонена.».
Отдельно, фоновая `BillingService` (не через бота) шлёт DM-предупреждение за 3 дня до истечения
оплаты и уведомление о приостановке конфигов при просрочке — оба через `NotifyUserAsync`, без
инлайн-кнопок.
## Команды и клавиатуры
| Команда / кнопка | Действие | Требует привязки |
@@ -213,6 +237,7 @@ Telegram ──updates──► TelegramBotHostedService → PnvBotUpdateHandl
| «✅ Активировать»/«❌ Отклонить» | (admin) решение по конкретному запросу активации | админ по env |
| `/requests` | (admin) список ожидающих запросов активации (до 10) | админ по env |
| «✅ Одобрить»/«❌ Отклонить» (`rrq:*`) | (admin) решение по заявке на роль — создаёт/назначает роль | админ по env |
| «✅ Подтвердить»/«❌ Отклонить» (`pay:*`) | (admin) решение по заявке на оплату — продлевает `BillingPaidUntil` | админ по env |
| «🌐 Открыть на сайте» | Ссылка на баг-репорт на сайте (только если задан `Telegram__PublicSiteUrl`) | админ по env |
Главное меню (`/start`/`/help`) — см. пункт 7 в «Возможности» выше.
+3 -2
View File
@@ -75,8 +75,9 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
### U2. Пользователь следит за трафиком
- Фоновая синхронизация тянет трафик из 3x-ui; изменения приходят в UI через SignalR (без перезагрузки).
- Это только отображение: лимиты по трафику/сроку конфига не реализованы — единственная квота —
число активных конфигов на роль. Конфиг живёт, пока его явно не отзовут.
- Это только отображение: лимиты по трафику не реализованы — единственная квота — число активных
конфигов на роль. Конфиг живёт, пока его явно не отзовут — если только его роль не подписана на
биллинг (см. domain-model.md), тогда неоплаченный конфиг может быть временно приостановлен.
### A1. Админ подключает ноду и публикует инбаунды
1. Вводит адрес панели 3x-ui, логин/пароль (шифруются при хранении).