feat: add student video and profile experience

This commit is contained in:
2026-07-28 15:23:42 +08:00
parent 5e993298e7
commit 7a3cf09a99
31 changed files with 19821 additions and 170 deletions

View File

@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using System.Text.Json;
using Tiku.Application.Content;
using Tiku.Application.Jobs;
using Tiku.Application.Security;
using Tiku.Application.Tenancy;
@@ -65,7 +66,7 @@ internal sealed class BackgroundJobService(
var result = await tenantExecutionScope.ExecuteAsync(
job.TenantId,
$"Background job {job.JobType}",
(_, token) => ProcessCoreAsync(job, token),
(provider, token) => ProcessCoreAsync(provider, job, token),
cancellationToken);
job.Status = BackgroundJobStatus.Succeeded;
job.CompletedAt = DateTimeOffset.UtcNow;
@@ -114,13 +115,16 @@ internal sealed class BackgroundJobService(
return jobs.Select(ToItem).ToArray();
}
private async Task<JsonElement> ProcessCoreAsync(BackgroundJob job, CancellationToken cancellationToken)
private async Task<JsonElement> ProcessCoreAsync(
IServiceProvider scopedProvider,
BackgroundJob job,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return job.JobType switch
{
"content_export" => await ProcessContentExportAsync(job, cancellationToken),
"content_import" => throw new NotSupportedException("content_import requires the module-specific importer before it can mutate content."),
"content_import" => await ProcessContentImportAsync(scopedProvider, job, cancellationToken),
"asset_security_scan" => throw new NotSupportedException("asset_security_scan requires a configured scanner provider before it can write scan results."),
"statistics_aggregation" => await ProcessStatisticsAggregationAsync(job, cancellationToken),
"commerce_reconciliation" => await ProcessCommerceReconciliationAsync(job, cancellationToken),
@@ -129,6 +133,47 @@ internal sealed class BackgroundJobService(
};
}
private static async Task<JsonElement> ProcessContentImportAsync(
IServiceProvider scopedProvider,
BackgroundJob job,
CancellationToken cancellationToken)
{
var directContentService = scopedProvider.GetRequiredService<IDirectContentService>();
var createdBy = GetJsonGuid(job.Payload, "createdBy") ?? Guid.Empty;
if (createdBy == Guid.Empty)
{
throw new InvalidOperationException("content_import job requires createdBy.");
}
var command = new SimpleImportCommand(
GetJsonString(job.Payload, "importType") ?? throw new InvalidOperationException("content_import job requires importType."),
GetJsonString(job.Payload, "sourceFormat"),
GetJsonString(job.Payload, "sourceName"),
GetJsonGuid(job.Payload, "regionId"),
GetJsonGuid(job.Payload, "entryId"),
GetJsonGuid(job.Payload, "contentNodeId"),
GetJsonGuid(job.Payload, "subjectId"),
GetJsonGuid(job.Payload, "categoryId"),
GetJsonGuid(job.Payload, "questionBankId"),
GetJsonGuid(job.Payload, "collectionId"),
GetJsonArray(job.Payload, "items"),
false);
var result = await directContentService.ExecuteImportAsync(
new DirectContentActor(job.TenantId, createdBy),
command,
cancellationToken);
return JsonSerializer.SerializeToElement(new
{
importJobId = result.Job.Id,
result.Job.ImportType,
result.Job.Status,
result.Job.TotalCount,
result.Job.InsertedCount,
result.Job.UpdatedCount,
result.Job.ErrorCount
});
}
private async Task<JsonElement> ProcessContentExportAsync(
BackgroundJob job,
CancellationToken cancellationToken)
@@ -300,6 +345,31 @@ internal sealed class BackgroundJobService(
: null;
}
private static Guid? GetJsonGuid(JsonElement element, string propertyName)
{
if (element.ValueKind != JsonValueKind.Object ||
!element.TryGetProperty(propertyName, out var property))
{
return null;
}
return property.ValueKind == JsonValueKind.String && Guid.TryParse(property.GetString(), out var value)
? value
: null;
}
private static IReadOnlyCollection<JsonElement> GetJsonArray(JsonElement element, string propertyName)
{
if (element.ValueKind != JsonValueKind.Object ||
!element.TryGetProperty(propertyName, out var property) ||
property.ValueKind != JsonValueKind.Array)
{
return [];
}
return property.EnumerateArray().Select(item => item.Clone()).ToArray();
}
private static DateOnly? GetJsonDateOnly(JsonElement element, string propertyName)
{
var value = GetJsonString(element, propertyName);