Files
tiku-backend.net/Tiku.Infrastructure/Content/PracticeBlueprints/PracticeBlueprintManagementService.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

156 lines
7.1 KiB
C#

using System.Text;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Catalog;
using Tiku.Application.Content;
using Tiku.Domain.Content;
using Tiku.Infrastructure.Security;
namespace Tiku.Infrastructure.Content;
internal sealed class PracticeBlueprintManagementService(ContentManagementDependencies dependencies)
: ContentManagementServiceBase(dependencies), IPracticeBlueprintManagementService
{
public async Task<CatalogList<PracticeBlueprintManagementItem>> GetPracticeBlueprintsAsync(
ContentManagementActor actor,
ContentManagementFilter filter,
CancellationToken cancellationToken = default)
{
var scope = await RequireDataScopeAsync(actor, cancellationToken);
var regionIds = scope.RegionIds.ToArray();
var query = questionBankPersistence.PracticeBlueprints
.AsNoTracking()
.Where(blueprint => blueprint.TenantId == actor.TenantId)
.ApplyDataScope(
scope,
blueprint => blueprint.CreatedBy == actor.UserId,
blueprint => blueprint.RegionId.HasValue && regionIds.Contains(blueprint.RegionId.Value));
if (!filter.IncludeInactive) query = query.Where(blueprint => blueprint.Status == ContentStatus.Active);
if (filter.RegionId.HasValue) query = query.Where(blueprint => blueprint.RegionId == filter.RegionId.Value);
if (filter.EntryId.HasValue) query = query.Where(blueprint => blueprint.EntryId == filter.EntryId.Value);
if (filter.NodeId.HasValue) query = query.Where(blueprint => blueprint.NodeId == filter.NodeId.Value);
if (filter.CollectionId.HasValue)
query = query.Where(blueprint => blueprint.CollectionId == filter.CollectionId.Value);
if (TryParse(filter.Mode, out PracticeMode mode)) query = query.Where(blueprint => blueprint.Mode == mode);
if (!string.IsNullOrWhiteSpace(filter.Keyword))
{
var keyword = filter.Keyword.Trim();
query = query.Where(blueprint => blueprint.Name.Contains(keyword));
}
var items = await query
.OrderBy(blueprint => blueprint.SortOrder)
.ThenBy(blueprint => blueprint.CreatedAt)
.Take(ResolveLimit(filter.Limit))
.Select(blueprint => ToBlueprintItem(blueprint))
.ToArrayAsync(cancellationToken);
return new CatalogList<PracticeBlueprintManagementItem>(items);
}
public async Task<ContentManagementResult<PracticeBlueprintManagementItem>> UpsertPracticeBlueprintAsync(
ContentManagementActor actor,
UpsertPracticeBlueprintCommand 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<QuestionCollection>(actor.TenantId, command.CollectionId, "collection_not_found",
cancellationToken);
var blueprint = await ResolveEntityByIdOrLegacyAsync(
questionBankPersistence.PracticeBlueprints,
actor.TenantId,
command.Id,
command.LegacyId,
cancellationToken);
var isNew = blueprint is null;
if (command.Id.HasValue && (blueprint is null || blueprint.Id != command.Id.Value))
throw new ContentManagementException("Practice blueprint was not found.", "practice_blueprint_not_found");
if (blueprint is not null && !scope.AllowsResource(actor.UserId, blueprint.CreatedBy, blueprint.RegionId))
throw new ContentManagementException("Practice blueprint was not found.", "practice_blueprint_not_found");
if (blueprint is null && !scope.AllowsResource(actor.UserId, actor.UserId, command.RegionId))
throw new ContentManagementException("Practice blueprint was not found.", "practice_blueprint_not_found");
blueprint ??= new PracticeBlueprint
{
Id = command.Id ?? Guid.NewGuid(),
TenantId = actor.TenantId,
CreatedBy = actor.UserId
};
blueprint.RegionId = command.RegionId;
blueprint.EntryId = command.EntryId;
blueprint.NodeId = command.NodeId;
blueprint.CollectionId = command.CollectionId;
blueprint.LegacyId = Normalize(command.LegacyId);
blueprint.Name = command.Name.Trim();
blueprint.Mode = Parse(command.Mode, PracticeMode.Sequential, "practice_mode_invalid");
blueprint.AssemblyType = Parse(command.AssemblyType, PracticeAssemblyType.Collection,
"practice_assembly_type_invalid");
blueprint.QuestionLimit = command.QuestionLimit;
blueprint.DurationMinutes = command.DurationMinutes;
blueprint.TotalScore = command.TotalScore;
blueprint.PassScore = command.PassScore;
blueprint.Sections = JsonArrayOrDefault(command.Sections);
blueprint.Rules = JsonObjectOrDefault(command.Rules);
blueprint.AccessRules = JsonObjectOrDefault(command.AccessRules);
blueprint.Status = Parse(command.Status, ContentStatus.Active, "content_status_invalid");
blueprint.SortOrder = command.Order ?? 0;
blueprint.UpdatedBy = actor.UserId;
if (isNew) questionBankPersistence.PracticeBlueprints.Add(blueprint);
await questionBankPersistence.SaveChangesAsync(cancellationToken);
return new ContentManagementResult<PracticeBlueprintManagementItem>(ToBlueprintItem(blueprint));
}
public ImportFieldMappingItem GetImportFieldMapping(string importType)
{
var spec = ResolveImportSpec(importType);
return new ImportFieldMappingItem(
spec.ImportType,
spec.Title,
spec.Description,
spec.Fields,
spec.Fields.Where(field => field.Required).Select(field => field.Field).ToArray());
}
public ImportTemplateItem GetImportTemplate(string importType, string? format)
{
var spec = ResolveImportSpec(importType);
var normalizedFormat = string.IsNullOrWhiteSpace(format) ? "json" : format.Trim().ToLowerInvariant();
var content = normalizedFormat switch
{
"json" => JsonSerializer.Serialize(spec.JsonExample, new JsonSerializerOptions { WriteIndented = true }),
"csv" => string.Join(
"\n",
spec.CsvRows.Select(row => string.Join(",", row.Select(EscapeCsv)))),
_ => throw new ContentManagementException("Import template format is not supported.",
"import_template_format_invalid")
};
return new ImportTemplateItem(
spec.ImportType,
normalizedFormat,
$"{spec.ImportType}-import-template.{normalizedFormat}",
normalizedFormat == "csv" ? "text/csv" : "application/json",
Convert.ToBase64String(Encoding.UTF8.GetBytes(content)),
content,
spec.Fields);
}
}