Implement support ticket system with role request and bug report functionalities
- Introduced a new support ticket system allowing users to submit bug reports and role requests. - Implemented endpoints for creating, updating, and managing support tickets, including file attachments. - Enhanced Telegram bot integration to handle role requests directly within the bot, enabling admins to approve or reject requests without accessing the website. - Updated database schema to include support ticket entities and their relationships. - Improved API documentation to reflect new support ticket endpoints and their usage. - Added necessary localization for support ticket features in both Russian and English.
This commit is contained in:
+53
-1
@@ -122,6 +122,56 @@ status, createdAt }`. `expiresAt` всегда `null` (лимиты по сро
|
||||
| GET | `/api/activation/status` | user | — | `{ isActivated, pendingRequest: { id, comment, createdAt } \| null }` |
|
||||
| POST | `/api/activation/request` | user | `{ comment? }` | `{ id, comment, createdAt }` |
|
||||
|
||||
## Support (пользователь)
|
||||
|
||||
Группа `/api/support`, `RequireAuthorization()` + `IRequiresActivation` (кроме `GET /attachments/{id}`,
|
||||
который тоже требует активации, но не привязан к типу тикета). Создание баг-репорта и добавление
|
||||
комментария — `multipart/form-data` (вложения), остальное — JSON.
|
||||
|
||||
| Метод | Путь | Тело запроса | Тело ответа |
|
||||
| ----- | ----------------------------------------- | ---------------------------------------------------------------------------- | ------------- |
|
||||
| GET | `/api/support/roles` | — | `RoleDto[]` (без `admin`) — для выбора существующей роли в заявке |
|
||||
| GET | `/api/support/tickets` | query: `type?, status?, page=1, pageSize=20` | `PagedList<TicketSummaryDto>` (только свои) |
|
||||
| GET | `/api/support/tickets/{id}` | — | `TicketDetailDto` (404, если не свой) |
|
||||
| POST | `/api/support/tickets/bug-reports` | multipart: `message` + `files[]` (до 5, изображения до 5 МБ) | `TicketDetailDto` |
|
||||
| POST | `/api/support/tickets/role-requests` | `{ existingRoleId? \| (newRoleName, newRoleMaxConfigs, newRoleMaxIpLimit), justification }` | `TicketDetailDto` |
|
||||
| POST | `/api/support/tickets/{id}/comments` | multipart: `body` + `files[]` | `TicketCommentDto` |
|
||||
| POST | `/api/support/tickets/{id}/reopen` | — | `204 No Content` (только владелец, только из `Resolved`) |
|
||||
| GET | `/api/support/attachments/{id}` | — | бинарный поток с `Content-Type` вложения |
|
||||
|
||||
`TicketSummaryDto`: `{ id, userId, userName, type, status, createdAt, lastActivityAt }` — один DTO для
|
||||
своего и админского списков. `TicketDetailDto` добавляет `requestedRoleId, requestedRoleName,
|
||||
proposedRoleName, proposedMaxConfigs, proposedMaxIpLimit, comments: TicketCommentDto[]`.
|
||||
`TicketCommentDto`: `{ id, authorId, authorName, body, createdAt, attachments: TicketAttachmentDto[] }`.
|
||||
|
||||
Ровно одна из двух заявок на роль: либо `existingRoleId` (роль `admin` запрещена — `403
|
||||
Support.CannotRequestAdminRole`), либо все три поля новой роли. Заявка при существующем открытом
|
||||
запросе на роль → `409 Support.RoleRequestAlreadyPending`. `POST …/comments` на `Closed`-тикете →
|
||||
`409 Support.TicketClosed`. Вложения отдаются не статикой — `<img src>` не может передать
|
||||
`Authorization`-заголовок, фронт качает их как `Blob` через `fetch` и рендерит `Object URL`.
|
||||
|
||||
## Admin — Support
|
||||
|
||||
Группа `/api/admin/support`, `RequireAuthorization(RoleNames.Admin)` (активация не проверяется — сеяный
|
||||
админ активирован всегда).
|
||||
|
||||
| Метод | Путь | Тело запроса | Тело ответа |
|
||||
| ----- | ------------------------------------------------ | ----------------------------------- | ------------- |
|
||||
| GET | `/api/admin/support/tickets` | query: `type?, status?, page, pageSize` | `PagedList<TicketSummaryDto>` (все пользователи) |
|
||||
| GET | `/api/admin/support/tickets/{id}` | — | `TicketDetailDto` |
|
||||
| POST | `/api/admin/support/tickets/{id}/comments` | multipart: `body` + `files[]` | `TicketCommentDto` |
|
||||
| POST | `/api/admin/support/tickets/{id}/resolve` | — | `204 No Content` (любой тип, только из `Open`) |
|
||||
| POST | `/api/admin/support/tickets/{id}/close` | — | `204 No Content` (любой тип, финал) |
|
||||
| POST | `/api/admin/support/tickets/{id}/approve` | — | `204 No Content` (только `RoleRequest`/`Open`; создаёt/назначает роль) |
|
||||
| POST | `/api/admin/support/tickets/{id}/reject` | `{ reason? }` | `204 No Content` (только `RoleRequest`; `reason` уходит комментарием) |
|
||||
|
||||
`approve`/`reject` — единственный способ решить заявку на роль (нельзя одобрить через `resolve`).
|
||||
При одобрении: если заявка на существующую роль — сразу `ChangeUserRoleCommand`-эквивалент; если на
|
||||
новую — сперва создаётся `AppRole` (`IRoleService.CreateRoleAsync`), затем назначается. То же самое
|
||||
администратор может сделать **из Telegram, не заходя на сайт** — инлайн-кнопки на уведомлении о
|
||||
заявке (см. [telegram-bot.md](telegram-bot.md)); для баг-репортов в Telegram только кнопка-ссылка
|
||||
на `/admin/support/{id}` — переписка и вложения только на сайте.
|
||||
|
||||
## Admin — Activation, Roles
|
||||
|
||||
| Метод | Путь | Роль | Тело запроса | Тело ответа |
|
||||
@@ -211,6 +261,8 @@ totalConfigs, activeConfigs, totalUsedUpBytes, totalUsedDownBytes }` — счи
|
||||
| `activationRequested` | `{ requestId, userId, userName, comment, createdAt }` | `admins` |
|
||||
| `userActivated` | `{ userId }` | владельцу |
|
||||
| `newsPublished` | `{ id, title, createdAt }` | все (broadcast) |
|
||||
| `ticketCreated` | `{ ticketId, userId, userName, type }` | `admins` |
|
||||
| `ticketUpdated` | `{ ticketId }` | владельцу |
|
||||
|
||||
### Client → Server
|
||||
Клиент только слушает; группировка по пользователю происходит на сервере при подключении, по
|
||||
@@ -224,7 +276,7 @@ totalConfigs, activeConfigs, totalUsedUpBytes, totalUsedDownBytes }` — счи
|
||||
| 401 | Нет/просрочен/невалиден access-токен |
|
||||
| 403 | Нет прав по роли, либо `Auth.NotActivated` |
|
||||
| 404 | Ресурс не найден |
|
||||
| 409 | Конфликт домена: `Configs.QuotaExceeded`, дубликат имени пользователя при регистрации, уже есть `Pending`-запрос активации |
|
||||
| 409 | Конфликт домена: `Configs.QuotaExceeded`, дубликат имени пользователя при регистрации, уже есть `Pending`-запрос активации, `Support.RoleRequestAlreadyPending`, `Support.TicketClosed` |
|
||||
| 422 | Прочие управляемые ошибки, не подошедшие под коды выше |
|
||||
| 429 | Rate limit (`/api/auth/*`, `/api/auth/telegram/*`, `/sub/{token}`) |
|
||||
| 500 | Необработанное исключение (перехватывается `UseExceptionHandler()`, тело без деталей) |
|
||||
|
||||
+12
-5
@@ -68,15 +68,16 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
|
||||
- **Commands / Queries** + их **Handlers** (`ICommandHandler<,>` / `IQueryHandler<,>` — свои интерфейсы),
|
||||
организованы по фичам (`Auth/Login/`, `Configs/Create/`, `Admin/Nodes/`, ...).
|
||||
- **Ports (интерфейсы)**: `IAppDbContext`, `IXuiPanelGateway`, `ICurrentUser`, `IIdentityService`,
|
||||
`ISecretProtector`, `IRealtimeNotifier`, `ITelegramNotifier`, `IRoleService`.
|
||||
`ISecretProtector`, `IRealtimeNotifier`, `ITelegramNotifier`, `IRoleService`, `IFileStorage`
|
||||
(вложения тикетов поддержки — диск в контейнере, см. `Infrastructure/Storage/DiskFileStorage`).
|
||||
- **Validators**: FluentValidation на команды, где есть что проверять помимо типов (не на все — см.
|
||||
[backend-conventions.md](backend-conventions.md)).
|
||||
- **DTO**: плоские `record`, конвертация из сущностей — статический метод `FromDomain(...)` на самом
|
||||
DTO, без маппера (Mapster/AutoMapper).
|
||||
- **Pipeline behaviors** (порядок: Logging → Validation → RequireActivation → UnitOfWork):
|
||||
`LoggingBehavior`, `ValidationBehavior`, `RequireActivationBehavior` (403 `Auth.NotActivated` для
|
||||
запросов с маркером `IRequiresActivation` — конфиги, новости, каталог приложений), `UnitOfWorkBehavior`
|
||||
(транзакция + `SaveChangesAsync` на команду). Отдельного `AuthorizationBehavior` для ролей нет —
|
||||
запросов с маркером `IRequiresActivation` — конфиги, новости, каталог приложений, тикеты поддержки),
|
||||
`UnitOfWorkBehavior` (транзакция + `SaveChangesAsync` на команду). Отдельного `AuthorizationBehavior` для ролей нет —
|
||||
роль проверяется через `RequireAuthorization()`/`RequireRole(...)` на эндпоинте либо явной проверкой
|
||||
в начале хендлера (например, «инбаунд доступен роли пользователя»).
|
||||
- **Result<T>**: явная модель успеха/ошибки (`Result`/`Result<T>`, `Error` с `ErrorType`) вместо
|
||||
@@ -97,6 +98,8 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
|
||||
- **Secrets**: `DataProtectionSecretProtector : ISecretProtector` (шифрование паролей нод at-rest,
|
||||
ASP.NET Core Data Protection, key-ring на томе `dp_keys`).
|
||||
- **Telegram**: `TelegramNotifier : ITelegramNotifier` — отправка DM-уведомлений через `ITelegramBotClient`.
|
||||
- **Storage**: `DiskFileStorage : IFileStorage` — вложения тикетов поддержки, файлы на диске под
|
||||
GUID-именем (`FileStorage:RootPath`, том `ticket_uploads` в docker-compose, как `dp_keys`).
|
||||
|
||||
> **SignalR-пуш физически лежит в `PnvPanel.Api/Hubs/`, не в `Infrastructure`.**
|
||||
> `SignalRRealtimeNotifier : IRealtimeNotifier` нужен `IHubContext<PanelHub>`, а сам `PanelHub`
|
||||
@@ -106,10 +109,14 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
|
||||
### 4. `PnvPanel.Api` (Presentation)
|
||||
Композиционный корень и транспорт.
|
||||
|
||||
- **Minimal API**-эндпоинты, сгруппированные по фичам — 12 файлов в `Endpoints/`
|
||||
- **Minimal API**-эндпоинты, сгруппированные по фичам — файлы в `Endpoints/`
|
||||
(`AuthEndpoints`, `ActivationEndpoints`, `ConfigEndpoints`, `AppEndpoints`, `SubscriptionEndpoints`,
|
||||
`TelegramEndpoints`, `AdminUserEndpoints`, `AdminAppEndpoints`, `AdminStatsEndpoints`, `NodeEndpoints`,
|
||||
`InboundEndpoints`, `RoleEndpoints`); полный список маршрутов — [api-design.md](api-design.md).
|
||||
`InboundEndpoints`, `RoleEndpoints`, `SupportEndpoints`, `AdminSupportEndpoints`); полный список
|
||||
маршрутов — [api-design.md](api-design.md). `SupportEndpoints`/`AdminSupportEndpoints` — первые в
|
||||
проекте с `multipart/form-data` (`[FromForm]` + `IFormFileCollection`, `.DisableAntiforgery()` —
|
||||
антифоржери-мидлварь в пайплайне не подключена, но ASP.NET Core минимал-API требует явно снять
|
||||
требование для form-эндпоинтов).
|
||||
Каждый эндпоинт аннотирован `.Produces<T>()`, чтобы OpenAPI-схема полностью описывала тело ответа
|
||||
(нужно для `pnpm gen:api` на фронте).
|
||||
- **SignalR Hubs**: `PanelHub` (`Hubs/`).
|
||||
|
||||
@@ -22,6 +22,10 @@ VpnConfig ─*─ TrafficSample (история трафика; пишетс
|
||||
AuditLog (append-only журнал действий; ссылается на ActorId/TargetId)
|
||||
ClientApp (каталог приложений-клиентов; группируется по OperatingSystem)
|
||||
NewsPost (лента новостей; публикуется админом, видна всем аутентифицированным пользователям)
|
||||
AppUser
|
||||
└─0..*─ SupportTicket (баг-репорт/предложение либо заявка на роль)
|
||||
└─1───*─ TicketComment (переписка; первое сообщение = описание/обоснование)
|
||||
└─0..*─ TicketAttachment (изображения, диск-хранилище)
|
||||
```
|
||||
|
||||
## Сущности
|
||||
@@ -272,6 +276,71 @@ UI **настойчиво напоминает** привязать его (ед
|
||||
Переходы: `Pending → Approved/Rejected/Expired`; `Approved → Consumed` (после выпуска JWT сайту).
|
||||
После `Consumed`/`Expired` — не переиспользуется.
|
||||
|
||||
### SupportTicket — обращение в поддержку
|
||||
Два вида: `BugReport` (свободная форма, с вложениями) и `RoleRequest` (запрос существующей роли —
|
||||
кроме `admin` — либо параметров новой). Текст/обоснование не хранится отдельным полем — это первое
|
||||
сообщение в переписке (`TicketComment`), созданное вместе с тикетом в одной операции.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------------- | ----------------- | ---------------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `UserId` | `Guid` | FK → AppUser (автор) |
|
||||
| `Type` | `TicketType` | `BugReport` / `RoleRequest` |
|
||||
| `Status` | `TicketStatus` | `Open` / `Resolved` / `Closed` |
|
||||
| `RequestedRoleId` | `Guid?` | Заполнено для `RoleRequest` при выборе существующей роли |
|
||||
| `ProposedRoleName` | `string?` | Заполнено для `RoleRequest` при запросе новой роли |
|
||||
| `ProposedMaxConfigs`| `int?` | Параметры новой роли (см. `AppRole.MaxConfigs`) |
|
||||
| `ProposedMaxIpLimit`| `int?` | Параметры новой роли (см. `AppRole.MaxIpLimit`) |
|
||||
| `CreatedAt` | `DateTimeOffset` | |
|
||||
|
||||
Инварианты и переходы (`backend/src/PnvPanel.Domain/Support/SupportTicket.cs`): `RequestedRoleId`
|
||||
и `Proposed*` никогда не заполнены одновременно — гарантируется отдельными фабриками
|
||||
(`CreateRoleRequestForExistingRole`/`CreateRoleRequestForNewRole`), а не runtime-проверкой.
|
||||
- `Resolve()` — только из `Open`. Для `RoleRequest` одобрение — оркестрация в Application
|
||||
(`ApproveRoleRequestCommandHandler`): при новой роли сначала `IRoleService.CreateRoleAsync`, затем
|
||||
в любом случае `ChangeUserRoleAsync` пользователю, и только потом `ticket.Resolve()`.
|
||||
- `Close()` — из `Open` или `Resolved`, **финал** (обратного пути нет). Для `RoleRequest` — отклонение.
|
||||
- `Reopen()` — только из `Resolved` (владелец тикета); `Closed` не переоткрывается.
|
||||
- Одновременно не более одной **открытой** заявки на роль (`Type == RoleRequest && Status == Open`)
|
||||
на пользователя — проверяется в Application, аналогично `ActivationRequest.AlreadyPending`.
|
||||
Баг-репорты такого ограничения не имеют.
|
||||
- Доступ — только активированному пользователю (`IRequiresActivation`, как и у конфигов/новостей);
|
||||
админские действия (resolve/close/approve/reject) идут по отдельным `/api/admin/support/*` с
|
||||
ролевой проверкой, без завязки на активацию.
|
||||
|
||||
### TicketComment — сообщение в переписке
|
||||
Плоская сущность (не навигационная коллекция на `SupportTicket` — конвенция проекта, см.
|
||||
`TrafficSample`), одна на любое сообщение (включая первое, созданное вместе с тикетом).
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ----------- | ---------------- | --------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `TicketId` | `Guid` | FK → SupportTicket |
|
||||
| `AuthorId` | `Guid` | FK → AppUser (владелец тикета либо админ) |
|
||||
| `Body` | `string` | |
|
||||
| `CreatedAt` | `DateTimeOffset` | |
|
||||
|
||||
Комментарий запрещён на `Closed`-тикете; на `Open`/`Resolved` — можно (для `Resolved` это не
|
||||
переоткрывает тикет автоматически, переоткрытие — отдельное явное действие пользователя `Reopen()`).
|
||||
|
||||
### TicketAttachment — вложение (изображение)
|
||||
Хранится на диске контейнера (`IFileStorage`/`DiskFileStorage`, volume `ticket_uploads` в
|
||||
docker-compose) — первая в проекте функциональность загрузки файлов. Вайтлист
|
||||
`image/jpeg|png|webp|gif`, до 5 МБ на файл, до 5 файлов на сообщение.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ---------------- | ---------------- | ------------------------------------------------------------------ |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `CommentId` | `Guid` | FK → TicketComment |
|
||||
| `FileName` | `string` | Оригинальное имя — только для отображения, не участвует в пути на диске |
|
||||
| `StoredFileName` | `string` | Серверное GUID-имя на диске (не доверяем пользовательскому вводу) |
|
||||
| `ContentType` | `string` | |
|
||||
| `SizeBytes` | `long` | |
|
||||
| `CreatedAt` | `DateTimeOffset` | |
|
||||
|
||||
Отдаётся авторизованным эндпоинтом (`GET /api/support/attachments/{id}`, проверка владения тикетом
|
||||
или роли admin), не статикой — вложения могут быть чувствительными.
|
||||
|
||||
## Value Objects
|
||||
|
||||
- **NodeCredentials** (`Nodes/NodeCredentials.cs`) — `Username` + `ProtectedPassword` (шифротекст,
|
||||
@@ -291,6 +360,8 @@ enum TelegramLoginStatus { Pending, Approved, Rejected, Expired, Consumed }
|
||||
enum ActivationStatus { Pending, Approved, Rejected }
|
||||
enum AuditSource { Web, Telegram, System }
|
||||
enum OsPlatform { IOS, Android, Windows, MacOS, Linux }
|
||||
enum TicketType { BugReport, RoleRequest }
|
||||
enum TicketStatus { Open, Resolved, Closed }
|
||||
```
|
||||
|
||||
## Уведомления и аудит (без диспетчера доменных событий)
|
||||
@@ -316,6 +387,12 @@ enum OsPlatform { IOS, Android, Windows, MacOS, Linux }
|
||||
| `PublishInboundCommandHandler` | `AuditLog` (`InboundPublished`/`InboundUnpublished`) |
|
||||
| `NodeHealthCheckService` (фон) | Обновляет `NodeStatus`; realtime `nodeStatusChanged` группе `admins` |
|
||||
| `TrafficSyncService` (фон) | `UpdateTraffic(...)`; realtime `configTrafficUpdated` владельцу |
|
||||
| `CreateBugReportTicketCommandHandler` | Realtime `ticketCreated` группе `admins`; Telegram админам — превью текста + кнопка-ссылка на сайт |
|
||||
| `CreateRoleRequestTicketCommandHandler` | Realtime `ticketCreated` группе `admins`; Telegram админам — инлайн-кнопки «Одобрить/Отклонить» |
|
||||
| `AddTicketCommentCommandHandler` | Realtime `ticketUpdated` владельцу, только если комментирует не он сам |
|
||||
| `ApproveRoleRequestCommandHandler` | Создаёт роль (если новая) + назначает пользователю; `AuditLog` (`RoleRequestApproved`); Telegram-DM владельцу |
|
||||
| `RejectRoleRequestCommandHandler` | `AuditLog` (`RoleRequestRejected`); Telegram-DM владельцу |
|
||||
| `ResolveTicketCommandHandler` / `CloseTicketCommandHandler` | `AuditLog` (`TicketResolved`/`TicketClosed`); realtime `ticketUpdated` владельцу |
|
||||
|
||||
SignalR-события и группы — см. [architecture.md](architecture.md#realtime-signalr) и
|
||||
[api-design.md](api-design.md#signalr--hub-hubspanel).
|
||||
|
||||
@@ -96,6 +96,8 @@
|
||||
| Лимит устройств (`limitIp`) | Квота роли (`AppRole.MaxIpLimit`; -1 = без лимита), применяется только к новым клиентам в 3x-ui |
|
||||
| Самоудаление аккаунта | Отзыв всех активных конфигов в 3x-ui + удаление `AppUser` |
|
||||
| Удаление пользователя админом | `DELETE /api/admin/users/{id}` — отзыв всех конфигов в 3x-ui + удаление `AppUser`; себя удалить нельзя |
|
||||
| Вложения тикетов поддержки | Диск в контейнере (`IFileStorage`/`DiskFileStorage`, volume `ticket_uploads`) — не S3, GUID-имена файлов |
|
||||
| Заявки на роль из бота | Одобрение/отклонение полностью в Telegram (`rrq:*`); баг-репорты — только ссылка на сайт |
|
||||
| Версионирование API | Без версий (`/api` без `v1`) |
|
||||
| Подписка (заголовки) | `Subscription-Userinfo` (used/total/expire) + `profile-update-interval` |
|
||||
| Тема сайта | Светлая + тёмная (+ системная); выбор в localStorage |
|
||||
|
||||
@@ -33,6 +33,12 @@ Telegram-бот — **второй канал доставки** (presentation-
|
||||
Зарегистрироваться»; плюс кнопка «🌐 Сайт панели» со ссылкой на сайт, если задан `Telegram__PublicSiteUrl`
|
||||
(пусто — кнопки нет). Слэш-команды `/configs`/`/unlink` продолжают работать как раньше — кнопки лишь
|
||||
вызывают те же обработчики через callback (`menu:configs`/`menu:unlink`/`menu:back`).
|
||||
8. **Админ: обработка заявок на роль поддержки** — при новой заявке (`SupportTicket.Type ==
|
||||
RoleRequest`) админ получает сообщение с описанием (существующая роль либо параметры новой) и
|
||||
обоснованием, жмёт «✅ Одобрить / ❌ Отклонить» **прямо в Telegram, без захода на сайт** — одобрение
|
||||
создаёт роль (если новая) и назначает её пользователю той же командой, что и на сайте. Баг-репорты/
|
||||
предложения — только уведомление с кнопкой-ссылкой на сайт, без инлайн-действий (переписка и
|
||||
вложения удобнее там).
|
||||
|
||||
**Не реализовано:**
|
||||
- QR-картинкой и агрегированная подписка в самом боте (только текстовая ссылка на конфиг по кнопке).
|
||||
@@ -41,6 +47,8 @@ Telegram-бот — **второй канал доставки** (presentation-
|
||||
- Webhook-транспорт — только long polling, конфигурации режима/URL в коде нет.
|
||||
- Полное самообслуживание (создание/ротация/отзыв конфигов) — бот **read-only** по конфигам (только
|
||||
просмотр списка и показ существующей ссылки по кнопке).
|
||||
- Баг-репорты/предложения тикетов поддержки **не решаются из бота** (только уведомление-ссылка) —
|
||||
ответы, вложения, resolve/close только на сайте.
|
||||
|
||||
## Размещение в архитектуре
|
||||
|
||||
@@ -162,6 +170,33 @@ Telegram ──updates──► TelegramBotHostedService → PnvBotUpdateHandl
|
||||
`/requests` — тот же список запросов по требованию (до 10 штук, `Pending`), с теми же кнопками; для
|
||||
кого он доступен — та же проверка админ-id.
|
||||
|
||||
## Флоу 6 — Обращения в поддержку
|
||||
|
||||
Два разных сценария в зависимости от типа тикета (`SupportTicket.Type`):
|
||||
|
||||
**Баг-репорт/предложение** — только уведомление, без действий в боте:
|
||||
1. `CreateBugReportTicketCommandHandler` вызывает
|
||||
`ITelegramNotifier.NotifyAdminsBugReportCreatedAsync(ticketId, userName, message, ct)`.
|
||||
2. Каждому админу уходит сообщение с превью текста (обрезано до ~300 символов) и, если задан
|
||||
`Telegram__PublicSiteUrl`, **кнопкой-ссылкой** `🌐 Открыть на сайте` на `/admin/support/{ticketId}`
|
||||
(`InlineKeyboardButton.WithUrl`, не callback) — тап открывает страницу тикета в браузере.
|
||||
3. Дальше — только на сайте: переписка, вложения, resolve/close.
|
||||
|
||||
**Заявка на роль** — полностью решается в Telegram:
|
||||
1. `CreateRoleRequestTicketCommandHandler` вызывает
|
||||
`ITelegramNotifier.NotifyAdminsRoleRequestCreatedAsync(ticketId, userName, roleDescription, justification, ct)`
|
||||
— `roleDescription` уже готовая строка (имя существующей роли либо «новая роль «X» (конфигов: N, IP: M)»).
|
||||
2. Сообщение с кнопками **«✅ Одобрить» / «❌ Отклонить»** (callback `rrq:approve:{id}`/`rrq:reject:{id}`
|
||||
— тот же 3-частный формат `prefix:action:guid`, что и `act:*` для активации).
|
||||
3. Нажатие → проверка прав (`TrySetAdminCurrentUserAsync`, тот же, что для активации) →
|
||||
`ApproveRoleRequestCommand`/`RejectRoleRequestCommand` (те же команды, что дёргает
|
||||
`POST /api/admin/support/tickets/{id}/approve|reject` на сайте). При одобрении — если роль новая,
|
||||
сперва создаётся `AppRole`, затем в любом случае назначается пользователю; тикет переходит в
|
||||
`Resolved`/`Closed`. Нажавшему админу — короткое подтверждение, исходное сообщение редактируется
|
||||
(дописывается статус), как и у `act:*`.
|
||||
4. Пользователю (если Telegram привязан) — DM «✅ Ваша заявка на роль одобрена.» / «❌ Ваша заявка на
|
||||
роль отклонена.».
|
||||
|
||||
## Команды и клавиатуры
|
||||
|
||||
| Команда / кнопка | Действие | Требует привязки |
|
||||
@@ -177,6 +212,8 @@ Telegram ──updates──► TelegramBotHostedService → PnvBotUpdateHandl
|
||||
| «📝 Зарегистрироваться» (`reg:new`) | Регистрация нового аккаунта прямо из бота (Флоу 3) | нет (нужно, чтобы **не** был привязан) |
|
||||
| «✅ Активировать»/«❌ Отклонить» | (admin) решение по конкретному запросу активации | админ по env |
|
||||
| `/requests` | (admin) список ожидающих запросов активации (до 10) | админ по env |
|
||||
| «✅ Одобрить»/«❌ Отклонить» (`rrq:*`) | (admin) решение по заявке на роль — создаёт/назначает роль | админ по env |
|
||||
| «🌐 Открыть на сайте» | Ссылка на баг-репорт на сайте (только если задан `Telegram__PublicSiteUrl`) | админ по env |
|
||||
|
||||
Главное меню (`/start`/`/help`) — см. пункт 7 в «Возможности» выше.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user