Enhance user and media management with sorting and statistics: add sorting options to user and media listing endpoints, implement media statistics retrieval, and update frontend components for sorting and displaying media processing times. Refactor related query handlers and API types to support new features.
build / backend (push) Successful in 1m35s
build / frontend (push) Successful in 31s
tests / backend-tests (push) Successful in 1m48s

This commit is contained in:
Leonid Pershin
2026-07-26 00:28:22 +03:00
parent d6ad8e11a4
commit 64a2dade74
27 changed files with 1634 additions and 39 deletions
@@ -9,5 +9,7 @@ public sealed record ListUsersQuery(
int PageSize,
string? Search,
Guid? RoleId,
bool? IsBlocked
bool? IsBlocked,
string? Sort = null,
bool Desc = false
) : IQuery<PagedList<UserSummaryDto>>;
@@ -17,6 +17,8 @@ public sealed class ListUsersQueryHandler(IIdentityService identityService)
query.Search,
query.RoleId,
query.IsBlocked,
query.Sort,
query.Desc,
cancellationToken
);
}
@@ -68,6 +68,8 @@ public interface IIdentityService
string? search,
Guid? roleId,
bool? isBlocked,
string? sort,
bool desc,
CancellationToken cancellationToken
);
@@ -8,5 +8,7 @@ public sealed record ListMediaAssetsQuery(
int Page,
int PageSize,
IReadOnlyList<MediaAssetStatus> Statuses,
string? Search
string? Search,
string? Sort = null,
bool Desc = false
) : IQuery<PagedList<MediaAssetDto>>;
@@ -30,9 +30,33 @@ public sealed class ListMediaAssetsQueryHandler(IAppDbContext dbContext)
var total = await q.CountAsync(cancellationToken);
// Сортировка по выбранному столбцу (в SQL, до пагинации); вторичный ключ — Id для стабильного
// порядка при равенстве значений (иначе страницы «дрожат»).
var ordered = (query.Sort?.ToLowerInvariant()) switch
{
"name" => query.Desc
? q.OrderByDescending(x => x.OriginalFileName)
: q.OrderBy(x => x.OriginalFileName),
"status" => query.Desc ? q.OrderByDescending(x => x.Status) : q.OrderBy(x => x.Status),
"duration" => query.Desc
? q.OrderByDescending(x => x.Duration)
: q.OrderBy(x => x.Duration),
"resolution" => query.Desc
? q.OrderByDescending(x => x.Width)
: q.OrderBy(x => x.Width),
"processing" => query.Desc
? q.OrderByDescending(x => x.ProcessingDuration)
: q.OrderBy(x => x.ProcessingDuration),
"created" => query.Desc
? q.OrderByDescending(x => x.CreatedAt)
: q.OrderBy(x => x.CreatedAt),
_ => q.OrderByDescending(x => x.CreatedAt),
};
// Маппинг в памяти: MediaAssetDto.From обращается к TimeSpan.TotalSeconds, который EF в SQL
// не переводит. Страница ограничена pageSize, поэтому материализация сущностей безопасна.
var entities = await q.OrderByDescending(x => x.CreatedAt)
var entities = await ordered
.ThenBy(x => x.Id)
.Skip((query.Page - 1) * query.PageSize)
.Take(query.PageSize)
.ToListAsync(cancellationToken);
@@ -14,6 +14,7 @@ public sealed record MediaAssetDto(
string? VideoCodec,
string? AudioCodec,
string? ErrorMessage,
double? ProcessingSeconds,
DateTimeOffset CreatedAt
)
{
@@ -30,6 +31,7 @@ public sealed record MediaAssetDto(
asset.VideoCodec,
asset.AudioCodec,
asset.ErrorMessage,
asset.ProcessingDuration?.TotalSeconds,
asset.CreatedAt
);
}
@@ -0,0 +1,11 @@
using LiteCqrs;
namespace TeleWave.Application.Media.Stats;
/// <summary>Сводка по обработке медиа: сколько сейчас в очереди/в обработке и среднее время обработки.</summary>
public sealed record GetMediaStatsQuery : IQuery<MediaStatsDto>;
/// <param name="Queued">Ассетов в статусе Pending (ждут обработки) сейчас.</param>
/// <param name="Processing">Ассетов в статусе Processing (обрабатываются) сейчас.</param>
/// <param name="AverageProcessingSeconds">Среднее время обработки по недавним завершённым (Ready), сек; null — если нет данных.</param>
public sealed record MediaStatsDto(int Queued, int Processing, double? AverageProcessingSeconds);
@@ -0,0 +1,40 @@
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Domain.Media;
namespace TeleWave.Application.Media.Stats;
public sealed class GetMediaStatsQueryHandler(IAppDbContext dbContext)
: IQueryHandler<GetMediaStatsQuery, MediaStatsDto>
{
/// <summary>По скольким последним завершённым ассетам усредняем время обработки.</summary>
private const int AverageSample = 500;
public async Task<MediaStatsDto> Handle(
GetMediaStatsQuery query,
CancellationToken cancellationToken
)
{
// Сгенерированные (ТВ-заставки) в статистику библиотеки не входят.
var assets = dbContext.MediaAssets.AsNoTracking().Where(x => x.Source != MediaSource.Generated);
var queued = await assets.CountAsync(x => x.Status == MediaAssetStatus.Pending, cancellationToken);
var processing = await assets.CountAsync(
x => x.Status == MediaAssetStatus.Processing,
cancellationToken
);
// Среднее — по недавним завершённым (ограничение выборки, чтобы не тянуть всю историю). EF не
// усредняет TimeSpan в SQL, поэтому берём длительности последних N и усредняем в памяти.
var recent = await assets
.Where(x => x.Status == MediaAssetStatus.Ready && x.ProcessingDuration != null)
.OrderByDescending(x => x.UpdatedAt)
.Select(x => x.ProcessingDuration!.Value)
.Take(AverageSample)
.ToListAsync(cancellationToken);
double? average = recent.Count > 0 ? recent.Average(d => d.TotalSeconds) : null;
return new MediaStatsDto(queued, processing, average);
}
}