Files
tiku-backend.net/Tiku.Infrastructure/Catalog/TaxonomyService.cs
xiong 33375a38d7
Some checks failed
ci / release-gate (push) Has been cancelled
refactor(architecture): harden module boundaries
2026-08-04 12:10:36 +08:00

157 lines
6.8 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Tiku.Application.Catalog;
using Tiku.Application.QuestionBanks;
using Tiku.Application.Security;
using Tiku.Domain.Catalog;
using Tiku.Domain.Content;
using Tiku.Domain.Tenancy;
using Tiku.Infrastructure.Persistence;
namespace Tiku.Infrastructure.Catalog;
public sealed class TaxonomyService(
ICatalogPersistence catalogPersistence,
IPublicQuestionAccessPolicy accessPolicy,
ITenantExecutionScope tenantExecutionScope) : ITaxonomyService
{
public async Task<IReadOnlyCollection<TaxonomyNodeItem>> ListAsync(
Guid tenantId,
CancellationToken cancellationToken = default)
{
var includePlatform = true;
try
{
await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken);
}
catch (PublicQuestionAccessDeniedException)
{
includePlatform = false;
}
return await tenantExecutionScope.ExecuteAsync(
new SystemScopeRequest(
tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(TaxonomyService),
"List platform taxonomy with tenant extensions", Guid.NewGuid().ToString("N")),
async (provider, token) =>
{
var systemCatalog = provider.GetRequiredService<ICatalogPersistence>();
var systemTenancy = provider.GetRequiredService<ITenancyPersistence>();
var platformTenantId = includePlatform
? await systemTenancy.Tenants.AsNoTracking()
.Where(tenant => tenant.Mode == TenantMode.PlatformOwned)
.Select(tenant => (Guid?)tenant.Id)
.SingleOrDefaultAsync(token)
: null;
return await systemCatalog.TaxonomyNodes.AsNoTracking()
.Where(node =>
node.IsActive &&
(node.TenantId == tenantId ||
(platformTenantId.HasValue && node.TenantId == platformTenantId.Value)))
.OrderBy(node => node.Depth)
.ThenBy(node => node.SortOrder)
.Select(node => new TaxonomyNodeItem(
node.Id,
node.TenantId == tenantId ? QuestionSource.Tenant : QuestionSource.Platform,
node.ParentId,
!node.ParentOwnerTenantId.HasValue
? null
: node.ParentOwnerTenantId == tenantId
? QuestionSource.Tenant
: QuestionSource.Platform,
node.NodeType,
node.Code,
node.Name,
node.Path,
node.Depth,
node.SortOrder,
node.Metadata))
.ToArrayAsync(token);
},
cancellationToken);
}
public async Task<TaxonomyNodeItem> CreateAsync(
Guid tenantId,
CreateTaxonomyNodeCommand command,
CancellationToken cancellationToken = default)
{
Guid? parentOwnerTenantId = null;
TaxonomyParent? parent = null;
if (command.ParentId.HasValue)
{
parentOwnerTenantId = command.ParentSource switch
{
QuestionSource.Tenant => tenantId,
QuestionSource.Platform => await ResolvePlatformTenantIdAsync(tenantId, cancellationToken),
_ => throw new InvalidOperationException("A parent source is required when parentId is provided.")
};
parent = await tenantExecutionScope.ExecuteAsync(
new SystemScopeRequest(
tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(TaxonomyService),
"Validate taxonomy extension parent ownership", Guid.NewGuid().ToString("N")),
async (provider, token) =>
{
var systemCatalog = provider.GetRequiredService<ICatalogPersistence>();
var systemTenancy = provider.GetRequiredService<ITenancyPersistence>();
return await systemCatalog.TaxonomyNodes.AsNoTracking()
.Where(node =>
node.TenantId == parentOwnerTenantId &&
node.Id == command.ParentId.Value &&
node.IsActive)
.Select(node => new TaxonomyParent(node.Path, node.Depth))
.SingleOrDefaultAsync(token);
},
cancellationToken)
?? throw new InvalidOperationException("Taxonomy parent was not found.");
}
var node = new TaxonomyNode
{
TenantId = tenantId,
ParentOwnerTenantId = parentOwnerTenantId,
ParentId = command.ParentId,
NodeType = command.NodeType,
Code = command.Code.Trim(),
Name = command.Name.Trim(),
Depth = parent is null ? 0 : parent.Depth + 1,
SortOrder = command.SortOrder,
Metadata = command.Metadata.Clone()
};
node.Path = parent is null
? $"n{node.Id:N}"
: $"{parent.Path}.n{node.Id:N}";
catalogPersistence.TaxonomyNodes.Add(node);
await catalogPersistence.SaveChangesAsync(cancellationToken);
return new TaxonomyNodeItem(
node.Id,
QuestionSource.Tenant,
node.ParentId,
node.ParentId.HasValue ? command.ParentSource : null,
node.NodeType,
node.Code,
node.Name,
node.Path,
node.Depth,
node.SortOrder,
node.Metadata);
}
private async Task<Guid> ResolvePlatformTenantIdAsync(Guid tenantId, CancellationToken cancellationToken)
{
await accessPolicy.EnsureCanStartAsync(tenantId, cancellationToken);
return await tenantExecutionScope.ExecuteAsync(
new SystemScopeRequest(
tenantId, SystemScopeCallerType.PublicQuestionBank, nameof(TaxonomyService),
"Resolve platform taxonomy owner", Guid.NewGuid().ToString("N")),
async (provider, token) => await provider.GetRequiredService<ITenancyPersistence>()
.Tenants.AsNoTracking()
.Where(tenant => tenant.Mode == TenantMode.PlatformOwned)
.Select(tenant => tenant.Id)
.SingleAsync(token),
cancellationToken);
}
private sealed record TaxonomyParent(string? Path, int Depth);
}