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.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user