Implement BumperEndpoints and remove deprecated bumper-related functionality
Added new BumperEndpoints to the API for managing bumper templates and variants, enhancing the channel management capabilities. Removed outdated bumper-related commands and handlers from the application, streamlining the codebase and improving maintainability. Updated ChannelEndpoints to reflect these changes and ensure proper routing for the new endpoints.
This commit is contained in:
+5
-12
@@ -5,22 +5,15 @@ using TeleWave.Application.Common.Models;
|
||||
namespace TeleWave.Application.Programming.Templates.CopyTemplate;
|
||||
|
||||
/// <summary>
|
||||
/// Копирует сетку канала на другой канал: слои, слоты, стыки и правила. Группы не копируются —
|
||||
/// они общие для всех каналов. Прежний шаблон канала-приёмника заменяется целиком.
|
||||
/// Копирует сетку канала на другой канал: слои, слоты и правила. Группы, стыки и заставки
|
||||
/// не копируются — они общие для всех каналов, копия ссылается на те же. Прежний шаблон
|
||||
/// канала-приёмника заменяется целиком.
|
||||
/// </summary>
|
||||
public sealed record CopyTemplateCommand(Guid SourceChannelId, Guid TargetChannelId)
|
||||
: ICommand<Result<CopyTemplateResultDto>>;
|
||||
|
||||
/// <summary>
|
||||
/// Что скопировалось. <paramref name="DroppedBumperRefs"/> — врезки-заставки, для которых на канале
|
||||
/// -приёмнике не нашлось блока с таким же именем: ссылка снята, врезку надо донастроить руками.
|
||||
/// </summary>
|
||||
public sealed record CopyTemplateResultDto(
|
||||
int Layers,
|
||||
int Slots,
|
||||
int Junctions,
|
||||
int DroppedBumperRefs
|
||||
);
|
||||
/// <summary>Что скопировалось.</summary>
|
||||
public sealed record CopyTemplateResultDto(int Layers, int Slots);
|
||||
|
||||
public sealed class CopyTemplateCommandValidator : AbstractValidator<CopyTemplateCommand>
|
||||
{
|
||||
|
||||
+13
-126
@@ -15,9 +15,10 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var target = await dbContext
|
||||
.Channels.Include(c => c.BumperTemplates)
|
||||
.FirstOrDefaultAsync(c => c.Id == command.TargetChannelId, cancellationToken);
|
||||
var target = await dbContext.Channels.FirstOrDefaultAsync(
|
||||
c => c.Id == command.TargetChannelId,
|
||||
cancellationToken
|
||||
);
|
||||
if (target is null)
|
||||
return Result.Failure<CopyTemplateResultDto>(ChannelErrors.NotFound);
|
||||
|
||||
@@ -30,138 +31,31 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
|
||||
if (source is null)
|
||||
return Result.Failure<CopyTemplateResultDto>(ChannelErrors.TemplateNotFound);
|
||||
|
||||
var sourceJunctions = await dbContext
|
||||
.JunctionTemplates.AsNoTracking()
|
||||
.Include(j => j.Elements)
|
||||
.Where(j => j.ChannelId == command.SourceChannelId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// Заставки живут на канале и на диске, поэтому не копируются: врезка ищет блок с таким же
|
||||
// именем у приёмника, а не найдя — остаётся без ссылки, и это возвращается в отчёте.
|
||||
var bumperByName = target
|
||||
.BumperTemplates.GroupBy(t => t.Name)
|
||||
.ToDictionary(g => g.Key, g => g.First().Id);
|
||||
var sourceBumperNames = await dbContext
|
||||
.Channels.AsNoTracking()
|
||||
.Where(c => c.Id == command.SourceChannelId)
|
||||
.SelectMany(c => c.BumperTemplates)
|
||||
.Select(t => new { t.Id, t.Name })
|
||||
.ToDictionaryAsync(t => t.Id, t => t.Name, cancellationToken);
|
||||
|
||||
var (junctionMap, droppedBumperRefs) = CopyJunctions(
|
||||
sourceJunctions,
|
||||
target.Id,
|
||||
sourceBumperNames,
|
||||
bumperByName
|
||||
);
|
||||
|
||||
// Прежняя сетка приёмника заменяется целиком: слить две сетки автоматически нельзя,
|
||||
// а «добавить поверх» дало бы кашу из пересекающихся слотов.
|
||||
var existing = await dbContext
|
||||
.ScheduleTemplates.Where(t => t.ChannelId == target.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
dbContext.ScheduleTemplates.RemoveRange(existing);
|
||||
await dbContext
|
||||
.JunctionTemplates.Where(j =>
|
||||
j.ChannelId == target.Id && !junctionMap.Values.Contains(j.Id)
|
||||
)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
var copyTemplate = ScheduleTemplate.Create(target.Id, source.Name);
|
||||
copyTemplate.SetFallbackGroup(source.FallbackGroupId);
|
||||
copyTemplate.SetRules(source.RulesJson);
|
||||
if (
|
||||
source.DefaultJunctionId is { } defaultJunction
|
||||
&& junctionMap.TryGetValue(defaultJunction, out var mappedDefault)
|
||||
)
|
||||
copyTemplate.SetDefaultJunction(mappedDefault);
|
||||
// Стыки и заставки общие для всех каналов — копия ссылается на те же, без перевешивания.
|
||||
copyTemplate.SetDefaultJunction(source.DefaultJunctionId);
|
||||
|
||||
var (layers, slots) = CopyGrid(source, copyTemplate, junctionMap);
|
||||
var (layers, slots) = CopyGrid(source, copyTemplate);
|
||||
|
||||
dbContext.ScheduleTemplates.Add(copyTemplate);
|
||||
target.SetTemplate(copyTemplate.Id);
|
||||
|
||||
return Result.Success(
|
||||
new CopyTemplateResultDto(layers, slots, junctionMap.Count, droppedBumperRefs)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Копирует стыки на канал-приёмник. Возвращает соответствие «стык источника → копия» (по нему
|
||||
/// потом перевешиваются ссылки слотов) и число врезок, потерявших ссылку на блок заставки.
|
||||
/// </summary>
|
||||
private (Dictionary<Guid, Guid> Map, int DroppedBumperRefs) CopyJunctions(
|
||||
IReadOnlyList<JunctionTemplate> sourceJunctions,
|
||||
Guid targetChannelId,
|
||||
IReadOnlyDictionary<Guid, string> sourceBumperNames,
|
||||
IReadOnlyDictionary<string, Guid> targetBumperByName
|
||||
)
|
||||
{
|
||||
var map = new Dictionary<Guid, Guid>();
|
||||
var dropped = 0;
|
||||
|
||||
foreach (var junction in sourceJunctions)
|
||||
{
|
||||
var copy = JunctionTemplate.Create(targetChannelId, junction.Name);
|
||||
map[junction.Id] = copy.Id;
|
||||
|
||||
foreach (var element in junction.Elements.OrderBy(e => e.Position))
|
||||
{
|
||||
var bumperTemplateId = MapBumper(
|
||||
element,
|
||||
sourceBumperNames,
|
||||
targetBumperByName,
|
||||
ref dropped
|
||||
);
|
||||
copy.AddElement(element.Kind)
|
||||
.Update(
|
||||
element.Kind,
|
||||
element.GroupId,
|
||||
bumperTemplateId,
|
||||
element.AmountMode,
|
||||
element.AmountValue,
|
||||
element.IsRequired,
|
||||
element.ConditionsJson
|
||||
);
|
||||
}
|
||||
|
||||
dbContext.JunctionTemplates.Add(copy);
|
||||
}
|
||||
|
||||
return (map, dropped);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Блок заставки у приёмника, соответствующий блоку источника по имени. Заставки живут на канале
|
||||
/// и на диске, поэтому не копируются: не нашлось одноимённого — врезка остаётся без ссылки,
|
||||
/// и это попадает в отчёт.
|
||||
/// </summary>
|
||||
private static Guid? MapBumper(
|
||||
JunctionElement element,
|
||||
IReadOnlyDictionary<Guid, string> sourceBumperNames,
|
||||
IReadOnlyDictionary<string, Guid> targetBumperByName,
|
||||
ref int dropped
|
||||
)
|
||||
{
|
||||
if (element.Kind != JunctionElementKind.Bumper)
|
||||
return null;
|
||||
|
||||
if (
|
||||
element.BumperTemplateId is { } sourceId
|
||||
&& sourceBumperNames.TryGetValue(sourceId, out var name)
|
||||
&& targetBumperByName.TryGetValue(name, out var mapped)
|
||||
)
|
||||
return mapped;
|
||||
|
||||
dropped++;
|
||||
return null;
|
||||
return Result.Success(new CopyTemplateResultDto(layers, slots));
|
||||
}
|
||||
|
||||
/// <summary>Переносит слои со слотами. Возвращает, сколько слоёв (кроме фонового) и слотов скопировано.</summary>
|
||||
private static (int Layers, int Slots) CopyGrid(
|
||||
ScheduleTemplate source,
|
||||
ScheduleTemplate copyTemplate,
|
||||
IReadOnlyDictionary<Guid, Guid> junctionMap
|
||||
ScheduleTemplate copyTemplate
|
||||
)
|
||||
{
|
||||
var layers = 0;
|
||||
@@ -179,7 +73,7 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
|
||||
|
||||
foreach (var slot in layer.Slots)
|
||||
{
|
||||
CopySlot(slot, copyLayer, junctionMap);
|
||||
CopySlot(slot, copyLayer);
|
||||
slots++;
|
||||
}
|
||||
}
|
||||
@@ -187,11 +81,7 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
|
||||
return (layers, slots);
|
||||
}
|
||||
|
||||
private static void CopySlot(
|
||||
Slot slot,
|
||||
GridLayer copyLayer,
|
||||
IReadOnlyDictionary<Guid, Guid> junctionMap
|
||||
)
|
||||
private static void CopySlot(Slot slot, GridLayer copyLayer)
|
||||
{
|
||||
var copySlot = copyLayer.AddSlot(
|
||||
slot.Title,
|
||||
@@ -220,12 +110,9 @@ public sealed class CopyTemplateCommandHandler(IAppDbContext dbContext)
|
||||
slot.BlockMode,
|
||||
slot.BlockValue,
|
||||
slot.OverflowPolicy,
|
||||
Map(slot.JunctionBetweenId, junctionMap),
|
||||
Map(slot.JunctionAfterId, junctionMap)
|
||||
slot.JunctionBetweenId,
|
||||
slot.JunctionAfterId
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static Guid? Map(Guid? id, IReadOnlyDictionary<Guid, Guid> map) =>
|
||||
id is { } value && map.TryGetValue(value, out var mapped) ? mapped : null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using TeleWave.Domain.Programming;
|
||||
|
||||
namespace TeleWave.Application.Programming.Templates;
|
||||
|
||||
@@ -11,7 +12,13 @@ public sealed record JunctionConditions(
|
||||
/// <summary>Ставить только при смене шоу, а не между сериями одного.</summary>
|
||||
bool OnlyOnElementChange = false,
|
||||
/// <summary>Не ставить чаще, чем раз в N минут (0 — без ограничения).</summary>
|
||||
int MinMinutesBetween = 0
|
||||
int MinMinutesBetween = 0,
|
||||
/// <summary>Только в эти дейпарты (пусто — в любые).</summary>
|
||||
IReadOnlyList<Daypart>? Dayparts = null,
|
||||
/// <summary>Только в это окно суток канала (null — в любое время).</summary>
|
||||
JunctionTimeWindow? TimeWindow = null,
|
||||
/// <summary>Вероятность показа в процентах; 100 — всегда.</summary>
|
||||
int Chance = 100
|
||||
)
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
@@ -21,6 +28,10 @@ public sealed record JunctionConditions(
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
/// <summary>Действует ли врезка в этом дейпарте.</summary>
|
||||
public bool AllowsDaypart(Daypart daypart) =>
|
||||
Dayparts is not { Count: > 0 } || Dayparts.Contains(daypart);
|
||||
|
||||
public string ToJson() => JsonSerializer.Serialize(this, Options);
|
||||
|
||||
public static JunctionConditions? FromJson(string? json)
|
||||
@@ -38,3 +49,6 @@ public sealed record JunctionConditions(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Окно суток канала; допускает переход через полночь («с 23:00 до 06:00»).</summary>
|
||||
public sealed record JunctionTimeWindow(TimeOnly From, TimeOnly To);
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ public sealed class AddJunctionElementCommandHandler(IAppDbContext dbContext)
|
||||
return Result.Failure<Guid>(TemplateErrors.JunctionNotFound);
|
||||
|
||||
var element = junction.AddElement(command.Kind);
|
||||
await JunctionLoader.MarkTemplateChangedAsync(dbContext, junction, cancellationToken);
|
||||
await JunctionLoader.MarkTemplatesChangedAsync(dbContext, junction.Id, cancellationToken);
|
||||
return Result.Success(element.Id);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-8
@@ -1,6 +1,4 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Domain.Programming;
|
||||
@@ -10,16 +8,13 @@ namespace TeleWave.Application.Programming.Templates.Junctions;
|
||||
public sealed class CreateJunctionCommandHandler(IAppDbContext dbContext)
|
||||
: ICommandHandler<CreateJunctionCommand, Result<Guid>>
|
||||
{
|
||||
public async Task<Result<Guid>> Handle(
|
||||
public 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);
|
||||
var junction = JunctionTemplate.Create(command.Name);
|
||||
dbContext.JunctionTemplates.Add(junction);
|
||||
return Result.Success(junction.Id);
|
||||
return Task.FromResult(Result.Success(junction.Id));
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -21,19 +21,19 @@ public sealed class DeleteJunctionCommandHandler(IAppDbContext dbContext)
|
||||
if (junction is null)
|
||||
return Result.Failure(TemplateErrors.JunctionNotFound);
|
||||
|
||||
// Слот, ссылающийся на удалённый стык, молча остался бы без врезок — проверяем заранее.
|
||||
var used = await dbContext.Slots.AnyAsync(
|
||||
// Стык общий: слот чужого канала, ссылающийся на удалённый стык, молча остался бы без врезок.
|
||||
var usedBySlot = await dbContext.Slots.AnyAsync(
|
||||
s => s.JunctionBetweenId == junction.Id || s.JunctionAfterId == junction.Id,
|
||||
cancellationToken
|
||||
);
|
||||
if (used)
|
||||
var usedByDefault = await dbContext.ScheduleTemplates.AnyAsync(
|
||||
t => t.DefaultJunctionId == junction.Id,
|
||||
cancellationToken
|
||||
);
|
||||
if (usedBySlot || usedByDefault)
|
||||
return Result.Failure(TemplateErrors.JunctionInUse);
|
||||
|
||||
dbContext.JunctionTemplates.Remove(junction);
|
||||
return await JunctionLoader.MarkTemplateChangedAsync(
|
||||
dbContext,
|
||||
junction,
|
||||
cancellationToken
|
||||
);
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
+32
-8
@@ -5,12 +5,12 @@ using TeleWave.Domain.Programming;
|
||||
|
||||
namespace TeleWave.Application.Programming.Templates.Junctions;
|
||||
|
||||
public sealed record ListJunctionsQuery(Guid ChannelId)
|
||||
: IQuery<IReadOnlyList<JunctionTemplateDto>>;
|
||||
public sealed record ListJunctionsQuery : IQuery<IReadOnlyList<JunctionTemplateDto>>;
|
||||
|
||||
public sealed record CreateJunctionCommand(Guid ChannelId, string Name) : ICommand<Result<Guid>>;
|
||||
public sealed record CreateJunctionCommand(string Name) : ICommand<Result<Guid>>;
|
||||
|
||||
public sealed record RenameJunctionCommand(Guid JunctionId, string Name) : ICommand<Result>;
|
||||
public sealed record UpdateJunctionCommand(Guid JunctionId, string Name, int? MaxTotalSeconds)
|
||||
: ICommand<Result>;
|
||||
|
||||
public sealed record DeleteJunctionCommand(Guid JunctionId) : ICommand<Result>;
|
||||
|
||||
@@ -22,9 +22,13 @@ public sealed record JunctionElementInput(
|
||||
JunctionElementKind Kind,
|
||||
Guid? GroupId,
|
||||
Guid? BumperTemplateId,
|
||||
Guid? BumperVariantId,
|
||||
JunctionAmountMode AmountMode,
|
||||
int AmountValue,
|
||||
bool IsRequired,
|
||||
/// <summary>Метка развилки: из врезок с одной меткой играет одна, выбранная по весам.</summary>
|
||||
string? ChoiceKey,
|
||||
int ChoiceWeight,
|
||||
JunctionConditions? Conditions
|
||||
);
|
||||
|
||||
@@ -37,17 +41,32 @@ public sealed record UpdateJunctionElementCommand(
|
||||
public sealed record RemoveJunctionElementCommand(Guid JunctionId, Guid ElementId)
|
||||
: ICommand<Result>;
|
||||
|
||||
public sealed record ReorderJunctionCommand(Guid JunctionId, IReadOnlyList<Guid> ElementIdsInOrder)
|
||||
: ICommand<Result>;
|
||||
/// <summary>
|
||||
/// Позиция врезки вместе с её развилкой: перетаскивание в цепочке одновременно меняет и порядок,
|
||||
/// и принадлежность к развилке, поэтому отдельной команды «сгруппировать» нет.
|
||||
/// </summary>
|
||||
public sealed record JunctionElementOrder(Guid ElementId, string? ChoiceKey);
|
||||
|
||||
public sealed record ReorderJunctionCommand(
|
||||
Guid JunctionId,
|
||||
IReadOnlyList<JunctionElementOrder> Order
|
||||
) : ICommand<Result>;
|
||||
|
||||
public sealed class CreateJunctionCommandValidator : AbstractValidator<CreateJunctionCommand>
|
||||
{
|
||||
public CreateJunctionCommandValidator() => RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
|
||||
}
|
||||
|
||||
public sealed class RenameJunctionCommandValidator : AbstractValidator<RenameJunctionCommand>
|
||||
public sealed class UpdateJunctionCommandValidator : AbstractValidator<UpdateJunctionCommand>
|
||||
{
|
||||
public RenameJunctionCommandValidator() => RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
|
||||
public UpdateJunctionCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Name).NotEmpty().MaximumLength(128);
|
||||
// Потолок стыка в сутки — верхняя граница здравого смысла, а не техническое ограничение.
|
||||
RuleFor(x => x.MaxTotalSeconds)
|
||||
.InclusiveBetween(1, 24 * 60 * 60)
|
||||
.When(x => x.MaxTotalSeconds is not null);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpdateJunctionElementCommandValidator
|
||||
@@ -57,8 +76,13 @@ public sealed class UpdateJunctionElementCommandValidator
|
||||
{
|
||||
// Верхняя граница — сутки: врезка длиннее вещательного дня бессмысленна.
|
||||
RuleFor(x => x.Input.AmountValue).InclusiveBetween(1, 24 * 60);
|
||||
RuleFor(x => x.Input.ChoiceKey).MaximumLength(64);
|
||||
RuleFor(x => x.Input.ChoiceWeight).InclusiveBetween(0, 1000);
|
||||
RuleFor(x => x.Input.Conditions!.MinMinutesBetween)
|
||||
.InclusiveBetween(0, 24 * 60)
|
||||
.When(x => x.Input.Conditions is not null);
|
||||
RuleFor(x => x.Input.Conditions!.Chance)
|
||||
.InclusiveBetween(0, 100)
|
||||
.When(x => x.Input.Conditions is not null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,14 +10,21 @@ public sealed record JunctionElementDto(
|
||||
string? GroupName,
|
||||
Guid? BumperTemplateId,
|
||||
string? BumperTemplateName,
|
||||
Guid? BumperVariantId,
|
||||
string? BumperVariantName,
|
||||
JunctionAmountMode AmountMode,
|
||||
int AmountValue,
|
||||
bool IsRequired,
|
||||
string? ChoiceKey,
|
||||
int ChoiceWeight,
|
||||
JunctionConditions? Conditions
|
||||
);
|
||||
|
||||
public sealed record JunctionTemplateDto(
|
||||
Guid Id,
|
||||
string Name,
|
||||
int? MaxTotalSeconds,
|
||||
/// <summary>Сколько каналов ссылается на стык — он общий, и это надо видеть до правки.</summary>
|
||||
int ChannelUsageCount,
|
||||
IReadOnlyList<JunctionElementDto> Elements
|
||||
);
|
||||
|
||||
@@ -17,18 +17,37 @@ internal static class JunctionLoader
|
||||
.JunctionTemplates.Include(j => j.Elements)
|
||||
.FirstOrDefaultAsync(j => j.Id == junctionId, cancellationToken);
|
||||
|
||||
/// <summary>Правка стыка — тоже правка правил эфира: шаблон канала помечается изменённым.</summary>
|
||||
public static async Task<Result> MarkTemplateChangedAsync(
|
||||
/// <summary>
|
||||
/// Правка стыка — правка правил эфира. Стык общий, поэтому изменёнными помечаются все шаблоны,
|
||||
/// которые на него ссылаются: иначе чужой канал молча поехал бы по новым врезкам без применения.
|
||||
/// </summary>
|
||||
public static async Task<Result> MarkTemplatesChangedAsync(
|
||||
IAppDbContext dbContext,
|
||||
JunctionTemplate junction,
|
||||
Guid junctionId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var template = await dbContext.ScheduleTemplates.FirstOrDefaultAsync(
|
||||
t => t.ChannelId == junction.ChannelId,
|
||||
cancellationToken
|
||||
);
|
||||
template?.MarkChanged();
|
||||
var viaSlots = await dbContext
|
||||
.Slots.AsNoTracking()
|
||||
.Where(s => s.JunctionBetweenId == junctionId || s.JunctionAfterId == junctionId)
|
||||
.Join(
|
||||
dbContext.GridLayers.AsNoTracking(),
|
||||
slot => slot.LayerId,
|
||||
layer => layer.Id,
|
||||
(_, layer) => layer.TemplateId
|
||||
)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var templates = await dbContext
|
||||
.ScheduleTemplates.Where(t =>
|
||||
viaSlots.Contains(t.Id) || t.DefaultJunctionId == junctionId
|
||||
)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var template in templates)
|
||||
template.MarkChanged();
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
|
||||
+69
-20
@@ -15,53 +15,102 @@ public sealed class ListJunctionsQueryHandler(IAppDbContext dbContext)
|
||||
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 groupIds = Ids(junctions, e => e.GroupId);
|
||||
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);
|
||||
var templateIds = Ids(junctions, e => e.BumperTemplateId);
|
||||
var bumpers = await dbContext
|
||||
.BumperTemplates.AsNoTracking()
|
||||
.Where(t => templateIds.Contains(t.Id))
|
||||
.Include(t => t.Variants)
|
||||
.ToListAsync(cancellationToken);
|
||||
var bumperNames = bumpers.ToDictionary(t => t.Id, t => t.Name);
|
||||
var variantNames = bumpers.SelectMany(t => t.Variants).ToDictionary(v => v.Id, v => v.Name);
|
||||
|
||||
var usage = await ChannelUsageAsync(cancellationToken);
|
||||
|
||||
return junctions
|
||||
.Select(j => new JunctionTemplateDto(
|
||||
j.Id,
|
||||
j.Name,
|
||||
j.MaxTotalSeconds,
|
||||
usage.GetValueOrDefault(j.Id),
|
||||
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,
|
||||
Lookup(groupNames, e.GroupId),
|
||||
e.BumperTemplateId,
|
||||
e.BumperTemplateId is { } bid && bumperNames.TryGetValue(bid, out var bname)
|
||||
? bname
|
||||
: null,
|
||||
Lookup(bumperNames, e.BumperTemplateId),
|
||||
e.BumperVariantId,
|
||||
Lookup(variantNames, e.BumperVariantId),
|
||||
e.AmountMode,
|
||||
e.AmountValue,
|
||||
e.IsRequired,
|
||||
e.ChoiceKey,
|
||||
e.ChoiceWeight,
|
||||
JunctionConditions.FromJson(e.ConditionsJson)
|
||||
))
|
||||
.ToList()
|
||||
))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>Сколько каналов ссылается на каждый стык — слотами сетки либо стыком по умолчанию.</summary>
|
||||
private async Task<Dictionary<Guid, int>> ChannelUsageAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var viaSlots = await (
|
||||
from slot in dbContext.Slots.AsNoTracking()
|
||||
join layer in dbContext.GridLayers.AsNoTracking() on slot.LayerId equals layer.Id
|
||||
join template in dbContext.ScheduleTemplates.AsNoTracking()
|
||||
on layer.TemplateId equals template.Id
|
||||
where slot.JunctionBetweenId != null || slot.JunctionAfterId != null
|
||||
select new
|
||||
{
|
||||
template.ChannelId,
|
||||
slot.JunctionBetweenId,
|
||||
slot.JunctionAfterId,
|
||||
}
|
||||
).ToListAsync(cancellationToken);
|
||||
|
||||
var viaDefault = await dbContext
|
||||
.ScheduleTemplates.AsNoTracking()
|
||||
.Where(t => t.DefaultJunctionId != null)
|
||||
.Select(t => new { t.ChannelId, t.DefaultJunctionId })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var pairs = viaSlots
|
||||
.SelectMany(s =>
|
||||
new[] { s.JunctionBetweenId, s.JunctionAfterId }
|
||||
.Where(id => id is not null)
|
||||
.Select(id => (JunctionId: id!.Value, s.ChannelId))
|
||||
)
|
||||
.Concat(viaDefault.Select(d => (JunctionId: d.DefaultJunctionId!.Value, d.ChannelId)));
|
||||
|
||||
return pairs.Distinct().GroupBy(p => p.JunctionId).ToDictionary(g => g.Key, g => g.Count());
|
||||
}
|
||||
|
||||
private static List<Guid> Ids(
|
||||
IEnumerable<Domain.Programming.JunctionTemplate> junctions,
|
||||
Func<Domain.Programming.JunctionElement, Guid?> selector
|
||||
) =>
|
||||
junctions
|
||||
.SelectMany(j => j.Elements)
|
||||
.Select(selector)
|
||||
.Where(id => id is not null)
|
||||
.Select(id => id!.Value)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
private static string? Lookup(IReadOnlyDictionary<Guid, string> names, Guid? id) =>
|
||||
id is { } value && names.TryGetValue(value, out var name) ? name : null;
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,9 +20,9 @@ public sealed class RemoveJunctionElementCommandHandler(IAppDbContext dbContext)
|
||||
if (junction is null || !junction.RemoveElement(command.ElementId))
|
||||
return Result.Failure(TemplateErrors.JunctionElementNotFound);
|
||||
|
||||
return await JunctionLoader.MarkTemplateChangedAsync(
|
||||
return await JunctionLoader.MarkTemplatesChangedAsync(
|
||||
dbContext,
|
||||
junction,
|
||||
junction.Id,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
+8
-3
@@ -20,10 +20,15 @@ public sealed class ReorderJunctionCommandHandler(IAppDbContext dbContext)
|
||||
if (junction is null)
|
||||
return Result.Failure(TemplateErrors.JunctionNotFound);
|
||||
|
||||
junction.Reorder(command.ElementIdsInOrder);
|
||||
return await JunctionLoader.MarkTemplateChangedAsync(
|
||||
// Порядок и принадлежность к развилке приезжают вместе: в цепочке это один жест мышью.
|
||||
foreach (var position in command.Order)
|
||||
if (junction.FindElement(position.ElementId) is { } element)
|
||||
element.SetChoice(position.ChoiceKey, element.ChoiceWeight);
|
||||
|
||||
junction.Reorder(command.Order.Select(o => o.ElementId));
|
||||
return await JunctionLoader.MarkTemplatesChangedAsync(
|
||||
dbContext,
|
||||
junction,
|
||||
junction.Id,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
+6
-6
@@ -4,11 +4,11 @@ using TeleWave.Application.Common.Models;
|
||||
|
||||
namespace TeleWave.Application.Programming.Templates.Junctions;
|
||||
|
||||
public sealed class RenameJunctionCommandHandler(IAppDbContext dbContext)
|
||||
: ICommandHandler<RenameJunctionCommand, Result>
|
||||
public sealed class UpdateJunctionCommandHandler(IAppDbContext dbContext)
|
||||
: ICommandHandler<UpdateJunctionCommand, Result>
|
||||
{
|
||||
public async Task<Result> Handle(
|
||||
RenameJunctionCommand command,
|
||||
UpdateJunctionCommand command,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
@@ -20,10 +20,10 @@ public sealed class RenameJunctionCommandHandler(IAppDbContext dbContext)
|
||||
if (junction is null)
|
||||
return Result.Failure(TemplateErrors.JunctionNotFound);
|
||||
|
||||
junction.Rename(command.Name);
|
||||
return await JunctionLoader.MarkTemplateChangedAsync(
|
||||
junction.Update(command.Name, command.MaxTotalSeconds);
|
||||
return await JunctionLoader.MarkTemplatesChangedAsync(
|
||||
dbContext,
|
||||
junction,
|
||||
junction.Id,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
+49
-27
@@ -1,6 +1,6 @@
|
||||
using LiteCqrs;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TeleWave.Application.Broadcast;
|
||||
using TeleWave.Application.Broadcast.Bumpers;
|
||||
using TeleWave.Application.Common.Interfaces;
|
||||
using TeleWave.Application.Common.Models;
|
||||
using TeleWave.Domain.Programming;
|
||||
@@ -25,38 +25,60 @@ public sealed class UpdateJunctionElementCommandHandler(IAppDbContext dbContext)
|
||||
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);
|
||||
}
|
||||
var check = await ValidateSourceAsync(input, cancellationToken);
|
||||
if (!check.IsSuccess)
|
||||
return check;
|
||||
|
||||
element.Update(
|
||||
input.Kind,
|
||||
input.GroupId,
|
||||
input.BumperTemplateId,
|
||||
input.AmountMode,
|
||||
input.AmountValue,
|
||||
input.IsRequired,
|
||||
input.Conditions?.ToJson()
|
||||
new JunctionElementSettings(
|
||||
input.Kind,
|
||||
input.GroupId,
|
||||
input.BumperTemplateId,
|
||||
input.BumperVariantId,
|
||||
input.AmountMode,
|
||||
input.AmountValue,
|
||||
input.IsRequired,
|
||||
input.ChoiceKey,
|
||||
input.ChoiceWeight,
|
||||
input.Conditions?.ToJson()
|
||||
)
|
||||
);
|
||||
|
||||
return await JunctionLoader.MarkTemplateChangedAsync(
|
||||
return await JunctionLoader.MarkTemplatesChangedAsync(
|
||||
dbContext,
|
||||
junction,
|
||||
junction.Id,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>Источник врезки должен существовать: молча пустая врезка выглядит как «стык не работает».</summary>
|
||||
private async Task<Result> ValidateSourceAsync(
|
||||
JunctionElementInput input,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (input.Kind != JunctionElementKind.Bumper)
|
||||
{
|
||||
if (input.GroupId is not { } groupId)
|
||||
return Result.Failure(TemplateErrors.JunctionGroupRequired);
|
||||
return await dbContext.Groups.AnyAsync(g => g.Id == groupId, cancellationToken)
|
||||
? Result.Success()
|
||||
: Result.Failure(TemplateErrors.GroupNotFound);
|
||||
}
|
||||
|
||||
if (input.BumperTemplateId is not { } templateId)
|
||||
return Result.Failure(BumperErrors.TemplateNotFound);
|
||||
if (!await dbContext.BumperTemplates.AnyAsync(t => t.Id == templateId, cancellationToken))
|
||||
return Result.Failure(BumperErrors.TemplateNotFound);
|
||||
|
||||
if (input.BumperVariantId is not { } variantId)
|
||||
return Result.Success();
|
||||
|
||||
return await dbContext.BumperTextVariants.AnyAsync(
|
||||
v => v.Id == variantId && v.BumperTemplateId == templateId,
|
||||
cancellationToken
|
||||
)
|
||||
? Result.Success()
|
||||
: Result.Failure(BumperErrors.VariantNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user