Add admin maintenance endpoints and file deletion functionality
- Introduced a new `/api/admin/maintenance` route for administrative maintenance tasks, requiring admin authorization. - Implemented the `DeleteAsync` method in `IFileStorage` to allow for the deletion of files associated with closed support tickets. - Updated API documentation to include details about the new maintenance operations and their effects on closed tickets. - Enhanced frontend routing to include the new maintenance section in the admin panel, improving navigation for administrators. - Added localization support for maintenance-related actions in both Russian and English.
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Application.Common.Models;
|
||||
|
||||
namespace PnvPanel.Application.Admin.Maintenance;
|
||||
|
||||
/// <summary>Удаляет все тикеты в статусе Closed вместе с их перепиской и вложениями (в т.ч. файлами
|
||||
/// на диске). Value — число удалённых тикетов.</summary>
|
||||
public sealed record DeleteClosedTicketsCommand : ICommand<Result<int>>;
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using PnvPanel.Application.Auth;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Application.Common.Models;
|
||||
using PnvPanel.Domain.Audit;
|
||||
using PnvPanel.Domain.Support;
|
||||
|
||||
namespace PnvPanel.Application.Admin.Maintenance;
|
||||
|
||||
public sealed class DeleteClosedTicketsCommandHandler(
|
||||
IAppDbContext dbContext,
|
||||
IFileStorage fileStorage,
|
||||
ICurrentUser currentUser
|
||||
) : ICommandHandler<DeleteClosedTicketsCommand, Result<int>>
|
||||
{
|
||||
public async Task<Result<int>> Handle(
|
||||
DeleteClosedTicketsCommand command,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (currentUser.UserId is not { } adminId)
|
||||
return Result.Failure<int>(AuthErrors.Unauthorized);
|
||||
|
||||
var ticketIds = await dbContext
|
||||
.SupportTickets.AsNoTracking()
|
||||
.Where(t => t.Status == TicketStatus.Closed)
|
||||
.Select(t => t.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (ticketIds.Count == 0)
|
||||
return Result.Success(0);
|
||||
|
||||
// Тикет/комментарий/вложение — плоские сущности без FK-каскада (см. SupportTicket), поэтому
|
||||
// порядок удаления важен: вложения (+ файлы на диске) -> комментарии -> тикеты.
|
||||
var comments = await dbContext
|
||||
.TicketComments.Where(c => ticketIds.Contains(c.TicketId))
|
||||
.ToListAsync(cancellationToken);
|
||||
var commentIds = comments.Select(c => c.Id).ToList();
|
||||
|
||||
var attachments = await dbContext
|
||||
.TicketAttachments.Where(a => commentIds.Contains(a.CommentId))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var attachment in attachments)
|
||||
await fileStorage.DeleteAsync(attachment.StoredFileName, cancellationToken);
|
||||
|
||||
dbContext.TicketAttachments.RemoveRange(attachments);
|
||||
dbContext.TicketComments.RemoveRange(comments);
|
||||
|
||||
var tickets = await dbContext
|
||||
.SupportTickets.Where(t => ticketIds.Contains(t.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
dbContext.SupportTickets.RemoveRange(tickets);
|
||||
|
||||
dbContext.AuditLogs.Add(
|
||||
AuditLog.Create(
|
||||
adminId,
|
||||
"ClosedTicketsCleanedUp",
|
||||
"SupportTicket",
|
||||
"bulk",
|
||||
metadata: $"count={ticketIds.Count}",
|
||||
AuditSource.Web
|
||||
)
|
||||
);
|
||||
|
||||
return Result.Success(ticketIds.Count);
|
||||
}
|
||||
}
|
||||
@@ -12,4 +12,7 @@ public interface IFileStorage
|
||||
|
||||
/// <summary>Null, если файла с таким именем нет на диске (например, удалён вручную).</summary>
|
||||
Task<Stream?> OpenReadAsync(string storedFileName, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Идемпотентно — отсутствие файла не ошибка (мог быть удалён вручную).</summary>
|
||||
Task DeleteAsync(string storedFileName, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user