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:
@@ -0,0 +1,94 @@
|
||||
using PnvPanel.Domain.Common;
|
||||
using PnvPanel.Domain.Exceptions;
|
||||
|
||||
namespace PnvPanel.Domain.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Обращение в поддержку: баг/предложение (свободная форма) либо заявка на роль (существующая роль
|
||||
/// или параметры новой). Текст обращения и переписка — в TicketComment, отдельной таблицей (не
|
||||
/// навигационная коллекция — см. конвенцию проекта на плоских сущностях, ср. TrafficSample/VpnConfig).
|
||||
/// Для RoleRequest заполнен либо RequestedRoleId, либо Proposed* — гарантируется отдельными фабриками,
|
||||
/// а не runtime-проверкой одного универсального конструктора.
|
||||
/// </summary>
|
||||
public sealed class SupportTicket : Entity
|
||||
{
|
||||
public Guid UserId { get; private set; }
|
||||
public TicketType Type { get; private set; }
|
||||
public TicketStatus Status { get; private set; }
|
||||
public Guid? RequestedRoleId { get; private set; }
|
||||
public string? ProposedRoleName { get; private set; }
|
||||
public int? ProposedMaxConfigs { get; private set; }
|
||||
public int? ProposedMaxIpLimit { get; private set; }
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
private SupportTicket()
|
||||
{
|
||||
}
|
||||
|
||||
public static SupportTicket CreateBugReport(Guid userId)
|
||||
{
|
||||
return new SupportTicket
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Type = TicketType.BugReport,
|
||||
Status = TicketStatus.Open,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
public static SupportTicket CreateRoleRequestForExistingRole(Guid userId, Guid roleId)
|
||||
{
|
||||
return new SupportTicket
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Type = TicketType.RoleRequest,
|
||||
Status = TicketStatus.Open,
|
||||
RequestedRoleId = roleId,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
public static SupportTicket CreateRoleRequestForNewRole(Guid userId, string name, int maxConfigs, int maxIpLimit)
|
||||
{
|
||||
return new SupportTicket
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Type = TicketType.RoleRequest,
|
||||
Status = TicketStatus.Open,
|
||||
ProposedRoleName = name,
|
||||
ProposedMaxConfigs = maxConfigs,
|
||||
ProposedMaxIpLimit = maxIpLimit,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Решено (в т.ч. заявка на роль одобрена — роль выдаётся оркестрацией на уровне Application).</summary>
|
||||
public void Resolve()
|
||||
{
|
||||
if (Status != TicketStatus.Open)
|
||||
throw new DomainException($"Нельзя перевести в Resolved тикет в статусе {Status}.");
|
||||
|
||||
Status = TicketStatus.Resolved;
|
||||
}
|
||||
|
||||
/// <summary>Финальное состояние — обратного пути нет (в т.ч. заявка на роль отклонена).</summary>
|
||||
public void Close()
|
||||
{
|
||||
if (Status == TicketStatus.Closed)
|
||||
throw new DomainException("Тикет уже закрыт.");
|
||||
|
||||
Status = TicketStatus.Closed;
|
||||
}
|
||||
|
||||
/// <summary>Только из Resolved — Closed финален и не переоткрывается.</summary>
|
||||
public void Reopen()
|
||||
{
|
||||
if (Status != TicketStatus.Resolved)
|
||||
throw new DomainException("Переоткрыть можно только решённый тикет.");
|
||||
|
||||
Status = TicketStatus.Open;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using PnvPanel.Domain.Common;
|
||||
|
||||
namespace PnvPanel.Domain.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Вложение (изображение) к сообщению тикета. StoredFileName — серверное имя на диске (GUID-based),
|
||||
/// FileName — оригинальное имя только для отображения (не участвует в построении пути — не доверяем
|
||||
/// пользовательскому вводу для файловой системы).
|
||||
/// </summary>
|
||||
public sealed class TicketAttachment : Entity
|
||||
{
|
||||
public Guid CommentId { get; private set; }
|
||||
public string FileName { get; private set; } = string.Empty;
|
||||
public string StoredFileName { get; private set; } = string.Empty;
|
||||
public string ContentType { get; private set; } = string.Empty;
|
||||
public long SizeBytes { get; private set; }
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
private TicketAttachment()
|
||||
{
|
||||
}
|
||||
|
||||
public static TicketAttachment Create(Guid commentId, string fileName, string storedFileName, string contentType, long sizeBytes)
|
||||
{
|
||||
return new TicketAttachment
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CommentId = commentId,
|
||||
FileName = fileName,
|
||||
StoredFileName = storedFileName,
|
||||
ContentType = contentType,
|
||||
SizeBytes = sizeBytes,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using PnvPanel.Domain.Common;
|
||||
|
||||
namespace PnvPanel.Domain.Support;
|
||||
|
||||
/// <summary>
|
||||
/// Сообщение в переписке по тикету — первое сообщение при создании тикета одновременно служит
|
||||
/// описанием бага/обоснованием заявки на роль (отдельного поля под это на SupportTicket нет).
|
||||
/// Вложения — TicketAttachment, отдельной таблицей по CommentId.
|
||||
/// </summary>
|
||||
public sealed class TicketComment : Entity
|
||||
{
|
||||
public Guid TicketId { get; private set; }
|
||||
public Guid AuthorId { get; private set; }
|
||||
public string Body { get; private set; } = string.Empty;
|
||||
public DateTimeOffset CreatedAt { get; private set; }
|
||||
|
||||
private TicketComment()
|
||||
{
|
||||
}
|
||||
|
||||
public static TicketComment Create(Guid ticketId, Guid authorId, string body)
|
||||
{
|
||||
return new TicketComment
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TicketId = ticketId,
|
||||
AuthorId = authorId,
|
||||
Body = body,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace PnvPanel.Domain.Support;
|
||||
|
||||
public enum TicketStatus
|
||||
{
|
||||
Open,
|
||||
Resolved,
|
||||
Closed,
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace PnvPanel.Domain.Support;
|
||||
|
||||
public enum TicketType
|
||||
{
|
||||
BugReport,
|
||||
RoleRequest,
|
||||
}
|
||||
Reference in New Issue
Block a user