using Microsoft.EntityFrameworkCore; using Tiku.Application.Catalog; using Tiku.Application.Content; using Tiku.Domain.Content; using Tiku.Infrastructure.Security; namespace Tiku.Infrastructure.Content; public sealed partial class ContentManagementService { public async Task> GetEntriesAsync( ContentManagementActor actor, ContentManagementFilter filter, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); var regionIds = scope.RegionIds.ToArray(); var query = dbContext.ContentEntries .AsNoTracking() .Where(entry => entry.TenantId == actor.TenantId) .ApplyDataScope( scope, entry => entry.CreatedBy == actor.UserId, entry => entry.RegionId.HasValue && regionIds.Contains(entry.RegionId.Value)); if (!filter.IncludeInactive) query = query.Where(entry => entry.IsActive); if (filter.RegionId.HasValue) query = query.Where(entry => entry.RegionId == filter.RegionId.Value); if (TryParse(filter.EntryType, out ContentEntryType entryType)) query = query.Where(entry => entry.EntryType == entryType); if (!string.IsNullOrWhiteSpace(filter.Keyword)) { var keyword = filter.Keyword.Trim(); query = query.Where(entry => entry.Name.Contains(keyword) || entry.EntryKey.Contains(keyword) || (entry.Description != null && entry.Description.Contains(keyword))); } var items = await query .OrderBy(entry => entry.SortOrder) .ThenBy(entry => entry.CreatedAt) .Take(ResolveLimit(filter.Limit)) .Select(entry => ToEntryItem(entry)) .ToArrayAsync(cancellationToken); return new CatalogList(items); } public async Task> UpsertEntryAsync( ContentManagementActor actor, UpsertContentEntryCommand command, CancellationToken cancellationToken = default) { var scope = await RequireDataScopeAsync(actor, cancellationToken); ArgumentException.ThrowIfNullOrWhiteSpace(command.Name); await AssertRegionAsync(actor.TenantId, command.RegionId, cancellationToken); var entryKey = Normalize(command.EntryKey) ?? Normalize(command.Id?.ToString("N")) ?? Guid.NewGuid().ToString("N"); var entry = await ResolveEntityAsync( dbContext.ContentEntries, actor.TenantId, command.Id, item => item.EntryKey == entryKey, cancellationToken); var isNew = entry is null; if (command.Id.HasValue && (entry is null || entry.Id != command.Id.Value)) throw new ContentManagementException("Content entry was not found.", "entry_not_found"); if (entry is not null && !scope.AllowsResource(actor.UserId, entry.CreatedBy, entry.RegionId)) throw new ContentManagementException("Content entry was not found.", "entry_not_found"); if (entry is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId)) throw new ContentManagementException("Content entry was not found.", "entry_not_found"); entry ??= new ContentEntry { Id = command.Id ?? Guid.NewGuid(), TenantId = actor.TenantId, EntryKey = entryKey, CreatedBy = actor.UserId }; entry.RegionId = command.RegionId; entry.LegacyId = Normalize(command.LegacyId); entry.Name = command.Name.Trim(); entry.EntryType = Parse(command.EntryType, ContentEntryType.QuestionPractice, "entry_type_invalid"); entry.Icon = Normalize(command.Icon); entry.Route = Normalize(command.Route); entry.Description = Normalize(command.Description); entry.Visibility = Parse(command.Visibility, ContentVisibility.Public, "visibility_invalid"); entry.AccessRules = JsonObjectOrDefault(command.AccessRules); entry.LayoutConfig = JsonObjectOrDefault(command.LayoutConfig); entry.SortOrder = command.Order ?? 0; entry.IsActive = command.IsActive ?? true; entry.UpdatedBy = actor.UserId; if (isNew) dbContext.ContentEntries.Add(entry); await dbContext.SaveChangesAsync(cancellationToken); return new ContentManagementResult(ToEntryItem(entry)); } }