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:
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user