refactor(architecture): harden module boundaries
Some checks failed
ci / release-gate (push) Has been cancelled

This commit is contained in:
2026-08-04 12:10:36 +08:00
parent b38f12e60b
commit 33375a38d7
218 changed files with 5124 additions and 3828 deletions

View File

@@ -0,0 +1,183 @@
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<CatalogList<QuestionCollectionManagementItem>> 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<QuestionCollectionManagementItem>(items);
}
public async Task<ContentManagementResult<QuestionCollectionManagementItem>> 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<Subject>(actor.TenantId, command.SubjectId, "subject_not_found", cancellationToken);
await AssertReferenceAsync<Category>(actor.TenantId, command.CategoryId, "category_not_found",
cancellationToken);
await AssertReferenceAsync<QuestionBank>(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<QuestionCollectionManagementItem>(ToCollectionItem(collection));
}
public async Task<CollectionItemsReplaceResult> 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());
}
}