Files
tiku-backend.net/Tiku.Infrastructure/Content/Foundation/DirectContentService.MappingAndValidation.cs
xiong c497a3ca8d
Some checks failed
ci / release-gate (push) Has been cancelled
清理代码
2026-08-03 12:31:39 +08:00

396 lines
12 KiB
C#

using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Tiku.Application.Assets;
using Tiku.Application.Content;
using Tiku.Application.Security;
using Tiku.Domain.Common;
using Tiku.Domain.Content;
using Tiku.Domain.Learning;
using Tiku.Domain.Operations;
using Tiku.Domain.QuestionBanks;
namespace Tiku.Infrastructure.Content;
public sealed partial class DirectContentService
{
private static QuestionManagementItem ToQuestionItem(Question question, QuestionVersion? version)
{
return new QuestionManagementItem(
question.Id,
version?.Id,
question.QuestionBankId,
question.SubjectId,
question.CategoryId,
question.NodeId,
question.EntryId,
question.ContentNodeId,
question.PrimaryCollectionId,
question.LegacyId,
question.Type,
question.TypeLabel,
question.Difficulty,
question.Tags,
version?.Content,
version?.Options ?? JsonDefaults.Array(),
version?.CorrectOptionIndex,
version?.CorrectOptionIndices ?? JsonDefaults.Array(),
version?.AnswerText,
version?.Explanation,
version?.SubQuestions ?? JsonDefaults.Array(),
version?.CodeLang,
version?.CodeTemplate,
question.MediaUrl,
question.HasVideoExplanation,
question.Status);
}
private static VideoManagementItem ToVideoItem(VideoExplanation item)
{
return new VideoManagementItem(
item.Id,
item.SubjectId,
item.LegacyId,
item.Title,
item.Description,
item.VideoUrl,
item.ThumbnailUrl,
item.DurationSeconds,
item.KnowledgeTags,
item.IsGeneral,
item.Difficulty,
item.SortOrder,
item.IsActive,
item.Metadata);
}
private static QuestionVideoManagementItem ToQuestionVideoItem(QuestionVideo item)
{
return new QuestionVideoManagementItem(
item.Id,
item.QuestionId,
item.VideoId,
item.LegacyId,
item.VideoType,
item.SortOrder,
item.Metadata);
}
private static OperationContentItem ToOperationItem(Banner item)
{
return new OperationContentItem(
item.Id,
"banners",
item.RegionId,
null,
item.LegacyId,
item.Title,
item.Content,
null,
null,
null,
null,
item.SortOrder,
item.IsActive,
JsonSerializer.SerializeToElement(new
{
item.Subtitle,
item.ButtonText,
item.ButtonLink,
item.BackgroundColor,
item.BorderColor
}));
}
private static OperationContentItem ToOperationItem(Faq item)
{
return new OperationContentItem(
item.Id,
"faqs",
item.RegionId,
null,
item.LegacyId,
null,
null,
item.Question,
item.Answer,
null,
null,
item.SortOrder,
item.IsActive,
JsonDefaults.Object());
}
private static OperationContentItem ToOperationItem(Announcement item)
{
return new OperationContentItem(
item.Id,
"announcements",
null,
null,
item.LegacyId,
null,
item.Content,
null,
null,
null,
null,
item.SortOrder,
item.IsActive,
JsonSerializer.SerializeToElement(new
{
item.Link,
item.BackgroundColor
}));
}
private static OperationContentItem ToOperationItem(ExamDate item)
{
return new OperationContentItem(
item.Id,
"exam-dates",
item.RegionId,
item.SchoolId,
item.LegacyId,
item.ExamName,
item.Description,
null,
null,
item.ExamAt,
item.ExamType,
item.SortOrder,
item.IsActive,
item.Metadata);
}
private static ContentImportJobItem ToJobItem(ContentImportJob job)
{
return new ContentImportJobItem(
job.Id,
job.TargetRegionId,
job.TargetSubjectId,
job.TargetCategoryId,
job.TargetContentNodeId,
job.TargetQuestionBankId,
job.ImportType,
job.SourceFormat,
job.Status,
job.SourceName,
job.SourceHash,
job.DryRun,
job.TotalCount,
job.ValidCount,
job.ErrorCount,
job.WarningCount,
job.InsertedCount,
job.UpdatedCount,
job.SkippedCount,
job.Summary,
job.ErrorMessage,
job.StartedAt,
job.FinishedAt,
job.CreatedAt,
job.UpdatedAt);
}
private static ContentImportItemModel ToImportItem(ContentImportItem item)
{
return new ContentImportItemModel(
item.Id,
item.JobId,
item.RowNo,
item.ExternalId,
item.Status,
item.TargetType,
item.TargetId,
item.SourcePayload,
item.NormalizedPayload,
item.ContentHash,
item.IssuesCount);
}
private async Task<CurrentDataScope> RequireDataScopeAsync(
DirectContentActor actor,
CancellationToken cancellationToken)
{
var access = await currentAccessContext.GetAsync(cancellationToken);
if (!access.IsCurrentTenantMember ||
access.UserId != actor.UserId ||
access.TenantId != actor.TenantId ||
!ContentPermissions.Any(access.HasTenantPermission))
throw new ContentManagementException("Tenant content access was denied.", "content_access_denied");
return access.DataScope;
}
private static void EnsureRegionWriteAllowed(
CurrentDataScope scope,
DirectContentActor actor,
Guid? currentRegionId,
Guid? targetRegionId,
bool isNew,
string notFoundCode)
{
var canAccessCurrent = isNew || scope.AllowsResource(actor.UserId, regionId: currentRegionId);
var canAccessTarget = scope.AllowsResource(actor.UserId, regionId: targetRegionId);
if (!canAccessCurrent || !canAccessTarget)
throw new ContentManagementException("Content resource was not found.", notFoundCode);
}
private async Task<TEntity?> ResolveByIdOrLegacyAsync<TEntity>(
DbSet<TEntity> set,
Guid tenantId,
Guid? id,
string? legacyId,
CancellationToken cancellationToken)
where TEntity : AuditableTenantEntity
{
if (id.HasValue)
return await set.SingleOrDefaultAsync(item => item.TenantId == tenantId && item.Id == id.Value,
cancellationToken);
var normalizedLegacyId = Normalize(legacyId);
return normalizedLegacyId is null
? null
: await set.SingleOrDefaultAsync(
item => item.TenantId == tenantId && EF.Property<string?>(item, "LegacyId") == normalizedLegacyId,
cancellationToken);
}
private async Task AssertReferenceAsync<TEntity>(
Guid tenantId,
Guid? id,
string code,
CancellationToken cancellationToken)
where TEntity : class
{
if (!id.HasValue) return;
var exists = await dbContext.Set<TEntity>()
.AnyAsync(
item => EF.Property<Guid>(item, "TenantId") == tenantId && EF.Property<Guid>(item, "Id") == id.Value,
cancellationToken);
if (!exists) throw new ContentManagementException("Referenced entity was not found.", code);
}
private async Task AssertImportJobAsync(Guid tenantId, Guid jobId, CancellationToken cancellationToken)
{
var exists = await dbContext.ContentImportJobs.AnyAsync(
item => item.TenantId == tenantId && item.Id == jobId,
cancellationToken);
if (!exists) throw new ContentManagementException("Import job was not found.", "import_job_not_found");
}
private static string NormalizeOperationKind(string kind)
{
var normalized = Normalize(kind)?.ToLowerInvariant();
return normalized switch
{
"banner" or "banners" => "banners",
"faq" or "faqs" => "faqs",
"announcement" or "announcements" => "announcements",
"exam-date" or "exam-dates" or "examdates" => "exam-dates",
_ => normalized ?? string.Empty
};
}
private static ContentImportType ParseImportType(string value)
{
return value.ToLowerInvariant() switch
{
"questions" => ContentImportType.Questions,
"vocabulary" => ContentImportType.Vocabulary,
"handbook" => ContentImportType.Handbook,
"scoreline" => ContentImportType.Scoreline,
"videos" => ContentImportType.Videos,
_ => throw new ContentManagementException("Import type is invalid.", "import_type_invalid")
};
}
private static TEnum Parse<TEnum>(string? value, TEnum fallback, string code)
where TEnum : struct
{
if (string.IsNullOrWhiteSpace(value)) return fallback;
if (Enum.TryParse<TEnum>(value.Trim(), true, out var parsed)) return parsed;
throw new ContentManagementException("Enum value is invalid.", code);
}
private static TEnum? ParseNullable<TEnum>(string? value, string code)
where TEnum : struct
{
if (string.IsNullOrWhiteSpace(value)) return null;
if (Enum.TryParse<TEnum>(value.Trim(), true, out var parsed)) return parsed;
throw new ContentManagementException("Enum value is invalid.", code);
}
private static string? Normalize(string? value)
{
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}
private static int ResolveLimit(int? limit)
{
return !limit.HasValue || limit <= 0 ? DefaultLimit : Math.Min(limit.Value, MaxLimit);
}
private static JsonElement JsonObjectOrDefault(JsonElement value)
{
return value.ValueKind is JsonValueKind.Object ? value : JsonDefaults.Object();
}
private static JsonElement JsonArrayOrDefault(JsonElement value)
{
return value.ValueKind is JsonValueKind.Array ? value : JsonDefaults.Array();
}
private static JsonElement GetElement(JsonElement payload, string name, JsonElement fallback)
{
return payload.ValueKind == JsonValueKind.Object && payload.TryGetProperty(name, out var value)
? value
: fallback;
}
private static string? GetString(JsonElement payload, string name)
{
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
return value.ValueKind == JsonValueKind.String ? Normalize(value.GetString()) : value.ToString();
}
private static int? GetInt(JsonElement payload, string name)
{
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
return value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number)
? number
: int.TryParse(value.ToString(), out number)
? number
: null;
}
private static Guid? GetGuid(JsonElement payload, string name)
{
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
return value.ValueKind == JsonValueKind.String &&
Guid.TryParse(value.GetString(), out var guid)
? guid
: null;
}
private static bool? GetBool(JsonElement payload, string name)
{
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
return value.ValueKind switch
{
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.String when bool.TryParse(value.GetString(), out var parsed) => parsed,
_ => null
};
}
}