Implement billing functionality and enhance role management
- 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:
+30
-4
@@ -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()`, тело без деталей) |
|
||||
|
||||
@@ -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
@@ -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
@@ -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, пароль генерируется и присылается в чат) |
|
||||
|
||||
@@ -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
@@ -75,8 +75,9 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
|
||||
|
||||
### U2. Пользователь следит за трафиком
|
||||
- Фоновая синхронизация тянет трафик из 3x-ui; изменения приходят в UI через SignalR (без перезагрузки).
|
||||
- Это только отображение: лимиты по трафику/сроку конфига не реализованы — единственная квота —
|
||||
число активных конфигов на роль. Конфиг живёт, пока его явно не отзовут.
|
||||
- Это только отображение: лимиты по трафику не реализованы — единственная квота — число активных
|
||||
конфигов на роль. Конфиг живёт, пока его явно не отзовут — если только его роль не подписана на
|
||||
биллинг (см. domain-model.md), тогда неоплаченный конфиг может быть временно приостановлен.
|
||||
|
||||
### A1. Админ подключает ноду и публикует инбаунды
|
||||
1. Вводит адрес панели 3x-ui, логин/пароль (шифруются при хранении).
|
||||
|
||||
Reference in New Issue
Block a user