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