using Microsoft.EntityFrameworkCore; using Tiku.Application.Catalog; using Tiku.Application.Content; using Tiku.Domain.Catalog; using Tiku.Domain.Content; using Tiku.Domain.QuestionBanks; using Tiku.Infrastructure.Security; namespace Tiku.Infrastructure.Content; internal sealed class QuestionCollectionManagementService(ContentManagementDependencies dependencies) : ContentManagementServiceBase(dependencies), IQuestionCollectionManagementService { public async Task> GetCollectionsAsync( ContentManagementActor actor, ContentManagementFilter filter, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); var regionIds = scope.RegionIds.ToArray(); var query = questionBankPersistence.QuestionCollections .AsNoTracking() .Where(collection => collection.TenantId == actor.TenantId) .ApplyDataScope( scope, collection => collection.CreatedBy == actor.UserId, collection => collection.RegionId.HasValue && regionIds.Contains(collection.RegionId.Value)); if (!filter.IncludeInactive) query = query.Where(collection => collection.Status == ContentStatus.Active); if (filter.RegionId.HasValue) query = query.Where(collection => collection.RegionId == filter.RegionId.Value); if (filter.EntryId.HasValue) query = query.Where(collection => collection.EntryId == filter.EntryId.Value); if (filter.NodeId.HasValue) query = query.Where(collection => collection.NodeId == filter.NodeId.Value); if (TryParse(filter.CollectionType, out QuestionCollectionType collectionType)) query = query.Where(collection => collection.CollectionType == collectionType); if (!string.IsNullOrWhiteSpace(filter.Keyword)) { var keyword = filter.Keyword.Trim(); query = query.Where(collection => collection.Name.Contains(keyword)); } var items = await query .OrderBy(collection => collection.SortOrder) .ThenBy(collection => collection.CreatedAt) .Take(ResolveLimit(filter.Limit)) .Select(collection => ToCollectionItem(collection)) .ToArrayAsync(cancellationToken); return new CatalogList(items); } public async Task> UpsertCollectionAsync( ContentManagementActor actor, UpsertQuestionCollectionCommand command, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); ArgumentException.ThrowIfNullOrWhiteSpace(command.Name); await AssertRegionAsync(actor.TenantId, command.RegionId, cancellationToken); await AssertEntryAsync(actor, scope, command.EntryId, cancellationToken); await AssertNodeAsync(actor, scope, command.NodeId, cancellationToken); await AssertReferenceAsync(actor.TenantId, command.SubjectId, "subject_not_found", cancellationToken); await AssertReferenceAsync(actor.TenantId, command.CategoryId, "category_not_found", cancellationToken); await AssertReferenceAsync(actor.TenantId, command.QuestionBankId, "question_bank_not_found", cancellationToken); var collection = await ResolveEntityByIdOrLegacyAsync( questionBankPersistence.QuestionCollections, actor.TenantId, command.Id, command.LegacyId, cancellationToken); var isNew = collection is null; if (command.Id.HasValue && (collection is null || collection.Id != command.Id.Value)) throw new ContentManagementException("Collection was not found.", "collection_not_found"); if (collection is not null && !scope.AllowsResource(actor.UserId, collection.CreatedBy, collection.RegionId)) throw new ContentManagementException("Collection was not found.", "collection_not_found"); if (collection is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId)) throw new ContentManagementException("Collection was not found.", "collection_not_found"); collection ??= new QuestionCollection { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId, CreatedBy = actor.UserId }; collection.RegionId = command.RegionId; collection.EntryId = command.EntryId; collection.NodeId = command.NodeId; collection.SubjectId = command.SubjectId; collection.CategoryId = command.CategoryId; collection.QuestionBankId = command.QuestionBankId; collection.LegacyId = Normalize(command.LegacyId); collection.Name = command.Name.Trim(); collection.CollectionType = Parse(command.CollectionType, QuestionCollectionType.Dynamic, "collection_type_invalid"); collection.SourceType = Parse(command.SourceType, QuestionCollectionSourceType.Filters, "collection_source_type_invalid"); collection.Filters = JsonObjectOrDefault(command.Filters); collection.TotalScore = command.TotalScore; collection.DurationMinutes = command.DurationMinutes; collection.Status = Parse(command.Status, ContentStatus.Active, "content_status_invalid"); collection.SortOrder = command.Order ?? 0; collection.AccessRules = JsonObjectOrDefault(command.AccessRules); collection.Metadata = JsonObjectOrDefault(command.Metadata); collection.UpdatedBy = actor.UserId; if (isNew) questionBankPersistence.QuestionCollections.Add(collection); await questionBankPersistence.SaveChangesAsync(cancellationToken); return new ContentManagementResult(ToCollectionItem(collection)); } public async Task ReplaceCollectionItemsAsync( ContentManagementActor actor, ReplaceCollectionItemsCommand command, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); var regionIds = scope.RegionIds.ToArray(); var collection = await questionBankPersistence.QuestionCollections .Where(item => item.TenantId == actor.TenantId && item.Id == command.CollectionId) .ApplyDataScope( scope, item => item.CreatedBy == actor.UserId, item => item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value)) .SingleOrDefaultAsync(cancellationToken); if (collection is null) throw new ContentManagementException("Collection was not found.", "collection_not_found"); var resolvedQuestions = new List<(CollectionQuestionCommand Command, TenantQuestionReference Reference)>(); foreach (var question in command.Questions) { var reference = await questionReferenceService.ResolveAsync( actor.TenantId, actor.UserId, question.Locator, cancellationToken); resolvedQuestions.Add((question, reference)); } var oldItems = await questionBankPersistence.QuestionCollectionItems .Where(item => item.TenantId == actor.TenantId && item.CollectionId == command.CollectionId) .ToArrayAsync(cancellationToken); questionBankPersistence.QuestionCollectionItems.RemoveRange(oldItems); var items = resolvedQuestions .Select((resolved, index) => new QuestionCollectionItem { TenantId = actor.TenantId, CollectionId = command.CollectionId, QuestionReferenceId = resolved.Reference.Id, QuestionOwnerTenantId = resolved.Reference.QuestionOwnerTenantId, QuestionId = resolved.Reference.QuestionId, SectionKey = Normalize(resolved.Command.SectionKey), SortOrder = resolved.Command.Order ?? index, Score = resolved.Command.Score, Required = resolved.Command.Required ?? true, Metadata = JsonObjectOrDefault(resolved.Command.Metadata) }) .ToArray(); questionBankPersistence.QuestionCollectionItems.AddRange(items); collection.QuestionCount = items.Length; collection.UpdatedBy = actor.UserId; await questionBankPersistence.SaveChangesAsync(cancellationToken); return new CollectionItemsReplaceResult( command.CollectionId, collection.QuestionCount, items.Select(ToCollectionItemItem).ToArray()); } }