Update scheduling parameters and refactor channel endpoints: extend HorizonDays to 7 and RetentionDays to 90 in appsettings.json. Consolidate channel-related endpoint logic by removing obsolete files and enhancing the ShowEndpoints with audience and genre management capabilities. Improve error handling and streamline command handlers for channel operations.
build / backend (push) Successful in 7m40s
build / frontend (push) Failing after 39s
tests / backend-tests (push) Successful in 6m9s

This commit is contained in:
Leonid Pershin
2026-07-26 13:32:13 +03:00
parent c4ef954dea
commit 66040a8841
272 changed files with 27944 additions and 8699 deletions
@@ -0,0 +1,64 @@
using FluentValidation;
using LiteCqrs;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Programming;
namespace TeleWave.Application.Programming.Templates.Junctions;
public sealed record ListJunctionsQuery(Guid ChannelId)
: IQuery<IReadOnlyList<JunctionTemplateDto>>;
public sealed record CreateJunctionCommand(Guid ChannelId, string Name) : ICommand<Result<Guid>>;
public sealed record RenameJunctionCommand(Guid JunctionId, string Name) : ICommand<Result>;
public sealed record DeleteJunctionCommand(Guid JunctionId) : ICommand<Result>;
public sealed record AddJunctionElementCommand(Guid JunctionId, JunctionElementKind Kind)
: ICommand<Result<Guid>>;
/// <summary>Полный набор настроек врезки — то же тело для правки любого элемента стыка.</summary>
public sealed record JunctionElementInput(
JunctionElementKind Kind,
Guid? GroupId,
Guid? BumperTemplateId,
JunctionAmountMode AmountMode,
int AmountValue,
bool IsRequired,
JunctionConditions? Conditions
);
public sealed record UpdateJunctionElementCommand(
Guid JunctionId,
Guid ElementId,
JunctionElementInput Input
) : ICommand<Result>;
public sealed record RemoveJunctionElementCommand(Guid JunctionId, Guid ElementId)
: ICommand<Result>;
public sealed record ReorderJunctionCommand(Guid JunctionId, IReadOnlyList<Guid> ElementIdsInOrder)
: ICommand<Result>;
public sealed class CreateJunctionCommandValidator : AbstractValidator<CreateJunctionCommand>
{
public CreateJunctionCommandValidator() => RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
}
public sealed class RenameJunctionCommandValidator : AbstractValidator<RenameJunctionCommand>
{
public RenameJunctionCommandValidator() => RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
}
public sealed class UpdateJunctionElementCommandValidator
: AbstractValidator<UpdateJunctionElementCommand>
{
public UpdateJunctionElementCommandValidator()
{
// Верхняя граница — сутки: врезка длиннее вещательного дня бессмысленна.
RuleFor(x => x.Input.AmountValue).InclusiveBetween(1, 24 * 60);
RuleFor(x => x.Input.Conditions!.MinMinutesBetween)
.InclusiveBetween(0, 24 * 60)
.When(x => x.Input.Conditions is not null);
}
}
@@ -0,0 +1,23 @@
using TeleWave.Domain.Programming;
namespace TeleWave.Application.Programming.Templates.Junctions;
public sealed record JunctionElementDto(
Guid Id,
int Position,
JunctionElementKind Kind,
Guid? GroupId,
string? GroupName,
Guid? BumperTemplateId,
string? BumperTemplateName,
JunctionAmountMode AmountMode,
int AmountValue,
bool IsRequired,
JunctionConditions? Conditions
);
public sealed record JunctionTemplateDto(
Guid Id,
string Name,
IReadOnlyList<JunctionElementDto> Elements
);
@@ -0,0 +1,297 @@
using LiteCqrs;
using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Broadcast;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Common.Models;
using TeleWave.Domain.Programming;
namespace TeleWave.Application.Programming.Templates.Junctions;
public sealed class ListJunctionsQueryHandler(IAppDbContext dbContext)
: IQueryHandler<ListJunctionsQuery, IReadOnlyList<JunctionTemplateDto>>
{
public async Task<IReadOnlyList<JunctionTemplateDto>> Handle(
ListJunctionsQuery query,
CancellationToken cancellationToken
)
{
var junctions = await dbContext
.JunctionTemplates.AsNoTracking()
.Include(j => j.Elements)
.Where(j => j.ChannelId == query.ChannelId)
.OrderBy(j => j.Name)
.ToListAsync(cancellationToken);
// Имена групп и блоков заставок резолвим одним проходом — редактор показывает их сразу.
var groupIds = junctions
.SelectMany(j => j.Elements)
.Select(e => e.GroupId)
.Where(id => id is not null)
.Select(id => id!.Value)
.Distinct()
.ToList();
var groupNames = await dbContext
.Groups.AsNoTracking()
.Where(g => groupIds.Contains(g.Id))
.ToDictionaryAsync(g => g.Id, g => g.Name, cancellationToken);
var bumperNames = await dbContext
.Channels.AsNoTracking()
.Where(c => c.Id == query.ChannelId)
.SelectMany(c => c.BumperTemplates)
.ToDictionaryAsync(t => t.Id, t => t.Name, cancellationToken);
return junctions
.Select(j => new JunctionTemplateDto(
j.Id,
j.Name,
j.Elements.OrderBy(e => e.Position)
.Select(e => new JunctionElementDto(
e.Id,
e.Position,
e.Kind,
e.GroupId,
e.GroupId is { } gid && groupNames.TryGetValue(gid, out var gname)
? gname
: null,
e.BumperTemplateId,
e.BumperTemplateId is { } bid && bumperNames.TryGetValue(bid, out var bname)
? bname
: null,
e.AmountMode,
e.AmountValue,
e.IsRequired,
JunctionConditions.FromJson(e.ConditionsJson)
))
.ToList()
))
.ToList();
}
}
public sealed class CreateJunctionCommandHandler(IAppDbContext dbContext)
: ICommandHandler<CreateJunctionCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(
CreateJunctionCommand command,
CancellationToken cancellationToken
)
{
if (!await dbContext.Channels.AnyAsync(c => c.Id == command.ChannelId, cancellationToken))
return Result.Failure<Guid>(ChannelErrors.NotFound);
var junction = JunctionTemplate.Create(command.ChannelId, command.Name);
dbContext.JunctionTemplates.Add(junction);
return Result.Success(junction.Id);
}
}
public sealed class RenameJunctionCommandHandler(IAppDbContext dbContext)
: ICommandHandler<RenameJunctionCommand, Result>
{
public async Task<Result> Handle(
RenameJunctionCommand command,
CancellationToken cancellationToken
)
{
var junction = await JunctionLoader.LoadAsync(
dbContext,
command.JunctionId,
cancellationToken
);
if (junction is null)
return Result.Failure(TemplateErrors.JunctionNotFound);
junction.Rename(command.Name);
return await MarkTemplateChangedAsync(dbContext, junction, cancellationToken);
}
/// <summary>Правка стыка — тоже правка правил эфира: шаблон канала помечается изменённым.</summary>
internal static async Task<Result> MarkTemplateChangedAsync(
IAppDbContext dbContext,
JunctionTemplate junction,
CancellationToken cancellationToken
)
{
var template = await dbContext.ScheduleTemplates.FirstOrDefaultAsync(
t => t.ChannelId == junction.ChannelId,
cancellationToken
);
template?.MarkChanged();
return Result.Success();
}
}
public sealed class DeleteJunctionCommandHandler(IAppDbContext dbContext)
: ICommandHandler<DeleteJunctionCommand, Result>
{
public async Task<Result> Handle(
DeleteJunctionCommand command,
CancellationToken cancellationToken
)
{
var junction = await JunctionLoader.LoadAsync(
dbContext,
command.JunctionId,
cancellationToken
);
if (junction is null)
return Result.Failure(TemplateErrors.JunctionNotFound);
// Слот, ссылающийся на удалённый стык, молча остался бы без врезок — проверяем заранее.
var used = await dbContext.Slots.AnyAsync(
s => s.JunctionBetweenId == junction.Id || s.JunctionAfterId == junction.Id,
cancellationToken
);
if (used)
return Result.Failure(TemplateErrors.JunctionInUse);
dbContext.JunctionTemplates.Remove(junction);
return await RenameJunctionCommandHandler.MarkTemplateChangedAsync(
dbContext,
junction,
cancellationToken
);
}
}
public sealed class AddJunctionElementCommandHandler(IAppDbContext dbContext)
: ICommandHandler<AddJunctionElementCommand, Result<Guid>>
{
public async Task<Result<Guid>> Handle(
AddJunctionElementCommand command,
CancellationToken cancellationToken
)
{
var junction = await JunctionLoader.LoadAsync(
dbContext,
command.JunctionId,
cancellationToken
);
if (junction is null)
return Result.Failure<Guid>(TemplateErrors.JunctionNotFound);
var element = junction.AddElement(command.Kind);
await RenameJunctionCommandHandler.MarkTemplateChangedAsync(
dbContext,
junction,
cancellationToken
);
return Result.Success(element.Id);
}
}
public sealed class UpdateJunctionElementCommandHandler(IAppDbContext dbContext)
: ICommandHandler<UpdateJunctionElementCommand, Result>
{
public async Task<Result> Handle(
UpdateJunctionElementCommand command,
CancellationToken cancellationToken
)
{
var junction = await JunctionLoader.LoadAsync(
dbContext,
command.JunctionId,
cancellationToken
);
var element = junction?.FindElement(command.ElementId);
if (junction is null || element is null)
return Result.Failure(TemplateErrors.JunctionElementNotFound);
var input = command.Input;
if (input.Kind == JunctionElementKind.Bumper)
{
var known = await dbContext
.Channels.Where(c => c.Id == junction.ChannelId)
.SelectMany(c => c.BumperTemplates)
.AnyAsync(t => t.Id == input.BumperTemplateId, cancellationToken);
if (!known)
return Result.Failure(ChannelErrors.BumperTemplateNotFound);
}
else
{
if (input.GroupId is not { } groupId)
return Result.Failure(TemplateErrors.JunctionGroupRequired);
if (!await dbContext.Groups.AnyAsync(g => g.Id == groupId, cancellationToken))
return Result.Failure(TemplateErrors.GroupNotFound);
}
element.Update(
input.Kind,
input.GroupId,
input.BumperTemplateId,
input.AmountMode,
input.AmountValue,
input.IsRequired,
input.Conditions?.ToJson()
);
return await RenameJunctionCommandHandler.MarkTemplateChangedAsync(
dbContext,
junction,
cancellationToken
);
}
}
public sealed class RemoveJunctionElementCommandHandler(IAppDbContext dbContext)
: ICommandHandler<RemoveJunctionElementCommand, Result>
{
public async Task<Result> Handle(
RemoveJunctionElementCommand command,
CancellationToken cancellationToken
)
{
var junction = await JunctionLoader.LoadAsync(
dbContext,
command.JunctionId,
cancellationToken
);
if (junction is null || !junction.RemoveElement(command.ElementId))
return Result.Failure(TemplateErrors.JunctionElementNotFound);
return await RenameJunctionCommandHandler.MarkTemplateChangedAsync(
dbContext,
junction,
cancellationToken
);
}
}
public sealed class ReorderJunctionCommandHandler(IAppDbContext dbContext)
: ICommandHandler<ReorderJunctionCommand, Result>
{
public async Task<Result> Handle(
ReorderJunctionCommand command,
CancellationToken cancellationToken
)
{
var junction = await JunctionLoader.LoadAsync(
dbContext,
command.JunctionId,
cancellationToken
);
if (junction is null)
return Result.Failure(TemplateErrors.JunctionNotFound);
junction.Reorder(command.ElementIdsInOrder);
return await RenameJunctionCommandHandler.MarkTemplateChangedAsync(
dbContext,
junction,
cancellationToken
);
}
}
internal static class JunctionLoader
{
public static Task<JunctionTemplate?> LoadAsync(
IAppDbContext dbContext,
Guid junctionId,
CancellationToken cancellationToken
) =>
dbContext
.JunctionTemplates.Include(j => j.Elements)
.FirstOrDefaultAsync(j => j.Id == junctionId, cancellationToken);
}