refactor(content): split content capability services
This commit is contained in:
@@ -23,6 +23,7 @@ internal static class ApiPresentationExtensions
|
||||
});
|
||||
services.AddProblemDetails();
|
||||
services.AddScoped<TenantAdminActorResolver>();
|
||||
services.AddScoped<DirectContentActorResolver>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
129
Tiku.Api/Controllers/ContentImportController.cs
Normal file
129
Tiku.Api/Controllers/ContentImportController.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("租户端-内容直接管理")]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/content")]
|
||||
public sealed class ContentImportController(
|
||||
IContentImportService service,
|
||||
IBackgroundJobQueue backgroundJobService,
|
||||
DirectContentActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpPost("imports/preview/{importType}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeatureFromRoute("importType")]
|
||||
[EndpointSummary("预览内容导入数据")]
|
||||
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SimpleImportResult>> PreviewImport(
|
||||
string importType,
|
||||
DirectImportDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.PreviewImportAsync(actorResolver.Resolve(), request.ToCommand(importType, true),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("imports/{importType}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeatureFromRoute("importType")]
|
||||
[EndpointSummary("执行或排队内容导入")]
|
||||
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<BackgroundJobItem>(StatusCodes.Status202Accepted)]
|
||||
public async Task<ActionResult<object>> ExecuteImport(
|
||||
string importType,
|
||||
DirectImportDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var actor = actorResolver.Resolve();
|
||||
var command = request.ToCommand(importType, false);
|
||||
if (request.Async == true || command.Items.Count > 100)
|
||||
{
|
||||
var job = await backgroundJobService.EnqueueAsync(
|
||||
new CreateBackgroundJobCommand(
|
||||
actor.TenantId,
|
||||
"content_import",
|
||||
JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
createdBy = actor.UserId,
|
||||
importType = command.ImportType,
|
||||
sourceFormat = command.SourceFormat,
|
||||
sourceName = command.SourceName,
|
||||
regionId = command.RegionId,
|
||||
entryId = command.EntryId,
|
||||
contentNodeId = command.ContentNodeId,
|
||||
subjectId = command.SubjectId,
|
||||
categoryId = command.CategoryId,
|
||||
questionBankId = command.QuestionBankId,
|
||||
collectionId = command.CollectionId,
|
||||
items = command.Items
|
||||
})),
|
||||
cancellationToken);
|
||||
return Accepted(job);
|
||||
}
|
||||
|
||||
return Ok(await service.ExecuteImportAsync(actor, command, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("imports/detail")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[EndpointSummary("查询内容导入任务详情")]
|
||||
[ProducesResponseType<ContentImportJobDetail>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentImportJobDetail>> GetImportDetail(
|
||||
[FromQuery] DirectImportJobDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetImportJobAsync(actorResolver.Resolve(), query.JobId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("imports/issues")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[EndpointSummary("查询内容导入问题明细")]
|
||||
[ProducesResponseType<CatalogList<ContentImportIssueModel>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ContentImportIssueModel>>> GetImportIssues(
|
||||
[FromQuery] DirectImportJobDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetImportIssuesAsync(actorResolver.Resolve(), query.JobId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("imports/post-check")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[EndpointSummary("执行内容导入后完整性检查")]
|
||||
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ImportPostCheckResult>> RunImportPostCheck(
|
||||
DirectImportJobDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.RunImportPostCheckAsync(actorResolver.Resolve(), request.JobId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("imports/post-check")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[EndpointSummary("查询内容导入后检查状态")]
|
||||
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ImportPostCheckResult>> GetImportPostCheck(
|
||||
[FromQuery] DirectImportJobDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetImportPostCheckAsync(actorResolver.Resolve(), query.JobId, cancellationToken));
|
||||
}
|
||||
}
|
||||
18
Tiku.Api/Controllers/DirectContentActorResolver.cs
Normal file
18
Tiku.Api/Controllers/DirectContentActorResolver.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
public sealed class DirectContentActorResolver(
|
||||
ICurrentUser currentUser,
|
||||
ITenantContext currentTenant)
|
||||
{
|
||||
internal DirectContentActor Resolve()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
throw new ContentManagementException("Tenant content actor was not resolved.",
|
||||
"tenant_content_access_denied");
|
||||
|
||||
return new DirectContentActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
71
Tiku.Api/Controllers/EducationCatalogManagementController.cs
Normal file
71
Tiku.Api/Controllers/EducationCatalogManagementController.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("租户端-内容直接管理")]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/content")]
|
||||
public sealed class EducationCatalogManagementController(
|
||||
IEducationCatalogManagementService service,
|
||||
DirectContentActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpGet("scoreline/schools")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线院校")]
|
||||
[ProducesResponseType<CatalogList<School>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<School>>> GetSchools(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetSchoolsAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/schools")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新分数线院校")]
|
||||
[ProducesResponseType<ContentManagementResult<School>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<School>>> UpsertSchool(
|
||||
DirectSchoolDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertSchoolAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/majors")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线专业")]
|
||||
[ProducesResponseType<CatalogList<Major>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<Major>>> GetMajors(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetMajorsAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/majors")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新分数线专业")]
|
||||
[ProducesResponseType<ContentManagementResult<Major>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<Major>>> UpsertMajor(
|
||||
DirectMajorDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertMajorAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
}
|
||||
105
Tiku.Api/Controllers/HandbookManagementController.cs
Normal file
105
Tiku.Api/Controllers/HandbookManagementController.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("租户端-内容直接管理")]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/content")]
|
||||
public sealed class HandbookManagementController(
|
||||
IHandbookManagementService service,
|
||||
DirectContentActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpGet("handbook-subjects")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询管理侧知识手册科目")]
|
||||
[ProducesResponseType<CatalogList<HandbookSubject>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<HandbookSubject>>> GetHandbookSubjects(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetHandbookSubjectsAsync(actorResolver.Resolve(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("handbook-subjects")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("新增或更新知识手册科目")]
|
||||
[ProducesResponseType<ContentManagementResult<HandbookSubject>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<HandbookSubject>>> UpsertHandbookSubject(
|
||||
DirectHandbookSubjectDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertHandbookSubjectAsync(actorResolver.Resolve(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("handbook-chapters")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询管理侧知识手册章节")]
|
||||
[ProducesResponseType<CatalogList<HandbookChapter>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<HandbookChapter>>> GetHandbookChapters(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetHandbookChaptersAsync(actorResolver.Resolve(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("handbook-chapters")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("新增或更新知识手册章节")]
|
||||
[ProducesResponseType<ContentManagementResult<HandbookChapter>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<HandbookChapter>>> UpsertHandbookChapter(
|
||||
DirectHandbookChapterDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertHandbookChapterAsync(actorResolver.Resolve(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("handbook-entries")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询管理侧知识手册条目")]
|
||||
[ProducesResponseType<CatalogList<HandbookEntry>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<HandbookEntry>>> GetHandbookEntries(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetHandbookEntriesAsync(actorResolver.Resolve(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("handbook-entries")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("新增或更新知识手册条目")]
|
||||
[ProducesResponseType<ContentManagementResult<HandbookEntry>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<HandbookEntry>>> UpsertHandbookEntry(
|
||||
DirectHandbookEntryDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertHandbookEntryAsync(actorResolver.Resolve(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
53
Tiku.Api/Controllers/OperationContentManagementController.cs
Normal file
53
Tiku.Api/Controllers/OperationContentManagementController.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("租户端-内容直接管理")]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/content")]
|
||||
public sealed class OperationContentManagementController(
|
||||
IOperationContentManagementService service,
|
||||
DirectContentActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpGet("operations/{kind}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantSiteContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("查询运营内容")]
|
||||
[ProducesResponseType<CatalogList<OperationContentItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<OperationContentItem>>> GetOperationContent(
|
||||
string kind,
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetOperationContentAsync(actorResolver.Resolve(), kind, query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("operations/{kind}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantSiteContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("新增或更新运营内容")]
|
||||
[ProducesResponseType<ContentManagementResult<OperationContentItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<OperationContentItem>>> UpsertOperationContent(
|
||||
string kind,
|
||||
DirectOperationContentDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertOperationContentAsync(actorResolver.Resolve(), kind, request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
51
Tiku.Api/Controllers/QuestionManagementController.cs
Normal file
51
Tiku.Api/Controllers/QuestionManagementController.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("租户端-内容直接管理")]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/content")]
|
||||
public sealed class QuestionManagementController(
|
||||
IQuestionManagementService service,
|
||||
DirectContentActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpPost("questions")]
|
||||
[Authorize(Policy = BackendPermissions.TenantContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[EndpointSummary("创建题目及首个版本")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> CreateQuestion(
|
||||
DirectQuestionWriteDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.CreateQuestionAsync(actorResolver.Resolve(), request.ToCommand(true),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPatch("questions")]
|
||||
[Authorize(Policy = BackendPermissions.TenantContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[EndpointSummary("更新题目并可选择创建新版本")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> UpdateQuestion(
|
||||
DirectQuestionWriteDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpdateQuestionAsync(actorResolver.Resolve(), request.ToCommand(false),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
101
Tiku.Api/Controllers/ScorelineManagementController.cs
Normal file
101
Tiku.Api/Controllers/ScorelineManagementController.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("租户端-内容直接管理")]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/content")]
|
||||
public sealed class ScorelineManagementController(
|
||||
IScorelineManagementService service,
|
||||
DirectContentActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpGet("scoreline/fields")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线字段")]
|
||||
[ProducesResponseType<CatalogList<ScorelineField>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ScorelineField>>> GetScorelineFields(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetScorelineFieldsAsync(actorResolver.Resolve(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/fields")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新动态分数线字段")]
|
||||
[ProducesResponseType<ContentManagementResult<ScorelineField>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<ScorelineField>>> UpsertScorelineField(
|
||||
DirectScorelineFieldDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertScorelineFieldAsync(actorResolver.Resolve(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/records")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线记录")]
|
||||
[ProducesResponseType<CatalogList<ScorelineRecord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ScorelineRecord>>> GetScorelineRecords(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetScorelineRecordsAsync(actorResolver.Resolve(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/records")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新分数线记录")]
|
||||
[ProducesResponseType<ContentManagementResult<ScorelineRecord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<ScorelineRecord>>> UpsertScorelineRecord(
|
||||
DirectScorelineRecordDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertScorelineRecordAsync(actorResolver.Resolve(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/years")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询分数线年份")]
|
||||
[ProducesResponseType<CatalogList<int>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<int>>> GetScorelineYears(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(
|
||||
await service.GetScorelineYearsAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/trend")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询分数线趋势摘要")]
|
||||
[ProducesResponseType<CatalogList<ScorelineTrendItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ScorelineTrendItem>>> GetScorelineTrend(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(
|
||||
await service.GetScorelineTrendAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
}
|
||||
@@ -1,499 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("租户端-内容直接管理")]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/content")]
|
||||
public sealed class TenantContentDirectController(
|
||||
IDirectContentService directContentService,
|
||||
IBackgroundJobQueue backgroundJobService,
|
||||
ICurrentUser currentUser,
|
||||
ITenantContext currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpPost("questions")]
|
||||
[Authorize(Policy = BackendPermissions.TenantContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[EndpointSummary("创建题目及首个版本")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> CreateQuestion(
|
||||
DirectQuestionWriteDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.CreateQuestionAsync(ResolveActor(), request.ToCommand(true),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPatch("questions")]
|
||||
[Authorize(Policy = BackendPermissions.TenantContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)]
|
||||
[EndpointSummary("更新题目并可选择创建新版本")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionManagementItem>>> UpdateQuestion(
|
||||
DirectQuestionWriteDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpdateQuestionAsync(ResolveActor(), request.ToCommand(false),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary-units")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("查询管理侧词汇单元")]
|
||||
[ProducesResponseType<CatalogList<VocabularyUnit>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<VocabularyUnit>>> GetVocabularyUnits(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetVocabularyUnitsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("vocabulary-units")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("新增或更新词汇单元")]
|
||||
[ProducesResponseType<ContentManagementResult<VocabularyUnit>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<VocabularyUnit>>> UpsertVocabularyUnit(
|
||||
DirectVocabularyUnitDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertVocabularyUnitAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary-words")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("查询管理侧词汇")]
|
||||
[ProducesResponseType<CatalogList<VocabularyWord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<VocabularyWord>>> GetVocabularyWords(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetVocabularyWordsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("vocabulary-words")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("新增或更新词汇")]
|
||||
[ProducesResponseType<ContentManagementResult<VocabularyWord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<VocabularyWord>>> UpsertVocabularyWord(
|
||||
DirectVocabularyWordDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertVocabularyWordAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("handbook-subjects")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询管理侧知识手册科目")]
|
||||
[ProducesResponseType<CatalogList<HandbookSubject>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<HandbookSubject>>> GetHandbookSubjects(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetHandbookSubjectsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("handbook-subjects")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("新增或更新知识手册科目")]
|
||||
[ProducesResponseType<ContentManagementResult<HandbookSubject>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<HandbookSubject>>> UpsertHandbookSubject(
|
||||
DirectHandbookSubjectDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertHandbookSubjectAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("handbook-chapters")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询管理侧知识手册章节")]
|
||||
[ProducesResponseType<CatalogList<HandbookChapter>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<HandbookChapter>>> GetHandbookChapters(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetHandbookChaptersAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("handbook-chapters")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("新增或更新知识手册章节")]
|
||||
[ProducesResponseType<ContentManagementResult<HandbookChapter>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<HandbookChapter>>> UpsertHandbookChapter(
|
||||
DirectHandbookChapterDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertHandbookChapterAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("handbook-entries")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("查询管理侧知识手册条目")]
|
||||
[ProducesResponseType<CatalogList<HandbookEntry>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<HandbookEntry>>> GetHandbookEntries(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetHandbookEntriesAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("handbook-entries")]
|
||||
[Authorize(Policy = BackendPermissions.TenantHandbookManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Handbook)]
|
||||
[EndpointSummary("新增或更新知识手册条目")]
|
||||
[ProducesResponseType<ContentManagementResult<HandbookEntry>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<HandbookEntry>>> UpsertHandbookEntry(
|
||||
DirectHandbookEntryDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertHandbookEntryAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/schools")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线院校")]
|
||||
[ProducesResponseType<CatalogList<School>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<School>>> GetSchools(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetSchoolsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/schools")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新分数线院校")]
|
||||
[ProducesResponseType<ContentManagementResult<School>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<School>>> UpsertSchool(
|
||||
DirectSchoolDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertSchoolAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/majors")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线专业")]
|
||||
[ProducesResponseType<CatalogList<Major>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<Major>>> GetMajors(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetMajorsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/majors")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新分数线专业")]
|
||||
[ProducesResponseType<ContentManagementResult<Major>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<Major>>> UpsertMajor(
|
||||
DirectMajorDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertMajorAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/fields")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线字段")]
|
||||
[ProducesResponseType<CatalogList<ScorelineField>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ScorelineField>>> GetScorelineFields(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetScorelineFieldsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/fields")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新动态分数线字段")]
|
||||
[ProducesResponseType<ContentManagementResult<ScorelineField>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<ScorelineField>>> UpsertScorelineField(
|
||||
DirectScorelineFieldDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertScorelineFieldAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/records")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询管理侧分数线记录")]
|
||||
[ProducesResponseType<CatalogList<ScorelineRecord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ScorelineRecord>>> GetScorelineRecords(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetScorelineRecordsAsync(ResolveActor(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("scoreline/records")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("新增或更新分数线记录")]
|
||||
[ProducesResponseType<ContentManagementResult<ScorelineRecord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<ScorelineRecord>>> UpsertScorelineRecord(
|
||||
DirectScorelineRecordDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertScorelineRecordAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/years")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询分数线年份")]
|
||||
[ProducesResponseType<CatalogList<int>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<int>>> GetScorelineYears(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(
|
||||
await directContentService.GetScorelineYearsAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("scoreline/trend")]
|
||||
[Authorize(Policy = BackendPermissions.TenantScorelineManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Scoreline)]
|
||||
[EndpointSummary("查询分数线趋势摘要")]
|
||||
[ProducesResponseType<CatalogList<ScorelineTrendItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ScorelineTrendItem>>> GetScorelineTrend(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(
|
||||
await directContentService.GetScorelineTrendAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("videos")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("查询租户视频解析")]
|
||||
[ProducesResponseType<CatalogList<VideoManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<VideoManagementItem>>> GetVideos(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetVideosAsync(ResolveActor(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("videos")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("新增或更新视频解析")]
|
||||
[ProducesResponseType<ContentManagementResult<VideoManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<VideoManagementItem>>> UpsertVideo(
|
||||
DirectVideoDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertVideoAsync(ResolveActor(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("question-videos")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("绑定题目与解析视频")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionVideoManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionVideoManagementItem>>> BindQuestionVideo(
|
||||
DirectQuestionVideoDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.BindQuestionVideoAsync(ResolveActor(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("operations/{kind}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantSiteContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("查询运营内容")]
|
||||
[ProducesResponseType<CatalogList<OperationContentItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<OperationContentItem>>> GetOperationContent(
|
||||
string kind,
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetOperationContentAsync(ResolveActor(), kind, query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("operations/{kind}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantSiteContentManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.SiteContent)]
|
||||
[EndpointSummary("新增或更新运营内容")]
|
||||
[ProducesResponseType<ContentManagementResult<OperationContentItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<OperationContentItem>>> UpsertOperationContent(
|
||||
string kind,
|
||||
DirectOperationContentDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.UpsertOperationContentAsync(ResolveActor(), kind, request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("imports/preview/{importType}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeatureFromRoute("importType")]
|
||||
[EndpointSummary("预览内容导入数据")]
|
||||
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<SimpleImportResult>> PreviewImport(
|
||||
string importType,
|
||||
DirectImportDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.PreviewImportAsync(ResolveActor(), request.ToCommand(importType, true),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("imports/{importType}")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeatureFromRoute("importType")]
|
||||
[EndpointSummary("执行或排队内容导入")]
|
||||
[ProducesResponseType<SimpleImportResult>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<BackgroundJobItem>(StatusCodes.Status202Accepted)]
|
||||
public async Task<ActionResult<object>> ExecuteImport(
|
||||
string importType,
|
||||
DirectImportDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var actor = ResolveActor();
|
||||
var command = request.ToCommand(importType, false);
|
||||
if (request.Async == true || command.Items.Count > 100)
|
||||
{
|
||||
var job = await backgroundJobService.EnqueueAsync(
|
||||
new CreateBackgroundJobCommand(
|
||||
actor.TenantId,
|
||||
"content_import",
|
||||
JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
createdBy = actor.UserId,
|
||||
importType = command.ImportType,
|
||||
sourceFormat = command.SourceFormat,
|
||||
sourceName = command.SourceName,
|
||||
regionId = command.RegionId,
|
||||
entryId = command.EntryId,
|
||||
contentNodeId = command.ContentNodeId,
|
||||
subjectId = command.SubjectId,
|
||||
categoryId = command.CategoryId,
|
||||
questionBankId = command.QuestionBankId,
|
||||
collectionId = command.CollectionId,
|
||||
items = command.Items
|
||||
})),
|
||||
cancellationToken);
|
||||
return Accepted(job);
|
||||
}
|
||||
|
||||
return Ok(await directContentService.ExecuteImportAsync(actor, command, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("imports/detail")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[EndpointSummary("查询内容导入任务详情")]
|
||||
[ProducesResponseType<ContentImportJobDetail>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentImportJobDetail>> GetImportDetail(
|
||||
[FromQuery] DirectImportJobDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetImportJobAsync(ResolveActor(), query.JobId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("imports/issues")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[EndpointSummary("查询内容导入问题明细")]
|
||||
[ProducesResponseType<CatalogList<ContentImportIssueModel>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ContentImportIssueModel>>> GetImportIssues(
|
||||
[FromQuery] DirectImportJobDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetImportIssuesAsync(ResolveActor(), query.JobId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("imports/post-check")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[EndpointSummary("执行内容导入后完整性检查")]
|
||||
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ImportPostCheckResult>> RunImportPostCheck(
|
||||
DirectImportJobDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.RunImportPostCheckAsync(ResolveActor(), request.JobId, cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("imports/post-check")]
|
||||
[Authorize(Policy = BackendPermissions.TenantJobManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[EndpointSummary("查询内容导入后检查状态")]
|
||||
[ProducesResponseType<ImportPostCheckResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ImportPostCheckResult>> GetImportPostCheck(
|
||||
[FromQuery] DirectImportJobDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await directContentService.GetImportPostCheckAsync(ResolveActor(), query.JobId, cancellationToken));
|
||||
}
|
||||
|
||||
private DirectContentActor ResolveActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
throw new ContentManagementException("Tenant content actor was not resolved.",
|
||||
"tenant_content_access_denied");
|
||||
|
||||
return new DirectContentActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
63
Tiku.Api/Controllers/VideoManagementController.cs
Normal file
63
Tiku.Api/Controllers/VideoManagementController.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("租户端-内容直接管理")]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/content")]
|
||||
public sealed class VideoManagementController(
|
||||
IVideoManagementService service,
|
||||
DirectContentActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpGet("videos")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("查询租户视频解析")]
|
||||
[ProducesResponseType<CatalogList<VideoManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<VideoManagementItem>>> GetVideos(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetVideosAsync(actorResolver.Resolve(), query.ToFilter(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("videos")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("新增或更新视频解析")]
|
||||
[ProducesResponseType<ContentManagementResult<VideoManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<VideoManagementItem>>> UpsertVideo(
|
||||
DirectVideoDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertVideoAsync(actorResolver.Resolve(), request.ToCommand(), cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("question-videos")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVideoManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Video)]
|
||||
[EndpointSummary("绑定题目与解析视频")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionVideoManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionVideoManagementItem>>> BindQuestionVideo(
|
||||
DirectQuestionVideoDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.BindQuestionVideoAsync(actorResolver.Resolve(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
77
Tiku.Api/Controllers/VocabularyManagementController.cs
Normal file
77
Tiku.Api/Controllers/VocabularyManagementController.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Api.Security;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Jobs;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Content;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Tags("租户端-内容直接管理")]
|
||||
[Produces("application/json")]
|
||||
[Route("api/tenant/content")]
|
||||
public sealed class VocabularyManagementController(
|
||||
IVocabularyManagementService service,
|
||||
DirectContentActorResolver actorResolver) : ControllerBase
|
||||
{
|
||||
[HttpGet("vocabulary-units")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("查询管理侧词汇单元")]
|
||||
[ProducesResponseType<CatalogList<VocabularyUnit>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<VocabularyUnit>>> GetVocabularyUnits(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetVocabularyUnitsAsync(actorResolver.Resolve(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("vocabulary-units")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("新增或更新词汇单元")]
|
||||
[ProducesResponseType<ContentManagementResult<VocabularyUnit>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<VocabularyUnit>>> UpsertVocabularyUnit(
|
||||
DirectVocabularyUnitDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertVocabularyUnitAsync(actorResolver.Resolve(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("vocabulary-words")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("查询管理侧词汇")]
|
||||
[ProducesResponseType<CatalogList<VocabularyWord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<VocabularyWord>>> GetVocabularyWords(
|
||||
[FromQuery] DirectContentQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.GetVocabularyWordsAsync(actorResolver.Resolve(), query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPut("vocabulary-words")]
|
||||
[Authorize(Policy = BackendPermissions.TenantVocabularyManage)]
|
||||
[Authorize(Policy = TikuPolicies.TenantAllDataScope)]
|
||||
[RequireSaasFeature(SaasFeatureCatalog.Vocabulary)]
|
||||
[EndpointSummary("新增或更新词汇")]
|
||||
[ProducesResponseType<ContentManagementResult<VocabularyWord>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<VocabularyWord>>> UpsertVocabularyWord(
|
||||
DirectVocabularyWordDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await service.UpsertVocabularyWordAsync(actorResolver.Resolve(), request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
}
|
||||
@@ -317,7 +317,7 @@ public sealed record OperationContentItem(
|
||||
|
||||
public sealed record ScorelineTrendItem(int Year, int SchoolCount, int MajorCount);
|
||||
|
||||
public interface IDirectContentService
|
||||
public interface IQuestionManagementService
|
||||
{
|
||||
Task<ContentManagementResult<QuestionManagementItem>> CreateQuestionAsync(DirectContentActor actor,
|
||||
QuestionWriteCommand command, CancellationToken cancellationToken = default);
|
||||
@@ -325,6 +325,10 @@ public interface IDirectContentService
|
||||
Task<ContentManagementResult<QuestionManagementItem>> UpdateQuestionAsync(DirectContentActor actor,
|
||||
QuestionWriteCommand command, CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
|
||||
public interface IVocabularyManagementService
|
||||
{
|
||||
Task<CatalogList<VocabularyUnit>> GetVocabularyUnitsAsync(DirectContentActor actor, AdminLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -337,6 +341,10 @@ public interface IDirectContentService
|
||||
Task<ContentManagementResult<VocabularyWord>> UpsertVocabularyWordAsync(DirectContentActor actor,
|
||||
VocabularyWordCommand command, CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
|
||||
public interface IHandbookManagementService
|
||||
{
|
||||
Task<CatalogList<HandbookSubject>> GetHandbookSubjectsAsync(DirectContentActor actor, AdminLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -355,6 +363,10 @@ public interface IDirectContentService
|
||||
Task<ContentManagementResult<HandbookEntry>> UpsertHandbookEntryAsync(DirectContentActor actor,
|
||||
HandbookEntryCommand command, CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
|
||||
public interface IEducationCatalogManagementService
|
||||
{
|
||||
Task<CatalogList<School>> GetSchoolsAsync(DirectContentActor actor, AdminLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -367,6 +379,10 @@ public interface IDirectContentService
|
||||
Task<ContentManagementResult<Major>> UpsertMajorAsync(DirectContentActor actor, MajorCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
|
||||
public interface IScorelineManagementService
|
||||
{
|
||||
Task<CatalogList<ScorelineField>> GetScorelineFieldsAsync(DirectContentActor actor, AdminLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -385,6 +401,10 @@ public interface IDirectContentService
|
||||
Task<CatalogList<ScorelineTrendItem>> GetScorelineTrendAsync(DirectContentActor actor, AdminLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
|
||||
public interface IVideoManagementService
|
||||
{
|
||||
Task<CatalogList<VideoManagementItem>> GetVideosAsync(DirectContentActor actor, AdminLimitFilter filter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -394,12 +414,20 @@ public interface IDirectContentService
|
||||
Task<ContentManagementResult<QuestionVideoManagementItem>> BindQuestionVideoAsync(DirectContentActor actor,
|
||||
QuestionVideoCommand command, CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
|
||||
public interface IOperationContentManagementService
|
||||
{
|
||||
Task<CatalogList<OperationContentItem>> GetOperationContentAsync(DirectContentActor actor, string kind,
|
||||
AdminLimitFilter filter, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ContentManagementResult<OperationContentItem>> UpsertOperationContentAsync(DirectContentActor actor,
|
||||
string kind, OperationContentCommand command, CancellationToken cancellationToken = default);
|
||||
|
||||
}
|
||||
|
||||
public interface IContentImportService
|
||||
{
|
||||
Task<SimpleImportResult> PreviewImportAsync(DirectContentActor actor, SimpleImportCommand command,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
@@ -417,4 +445,4 @@ public interface IDirectContentService
|
||||
|
||||
Task<ImportPostCheckResult> GetImportPostCheckAsync(DirectContentActor actor, Guid jobId,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService(
|
||||
TikuDbContext dbContext,
|
||||
IQuestionReferenceService questionReferenceService,
|
||||
ICurrentAccessContext currentAccessContext,
|
||||
IFeatureAccessService featureAccessService) : IDirectContentService
|
||||
{
|
||||
private const int DefaultLimit = 100;
|
||||
private const int MaxLimit = 1000;
|
||||
|
||||
private static readonly string[] ContentPermissions =
|
||||
[
|
||||
BackendPermissions.TenantContentManage,
|
||||
BackendPermissions.TenantVocabularyManage,
|
||||
BackendPermissions.TenantHandbookManage,
|
||||
BackendPermissions.TenantVideoManage,
|
||||
BackendPermissions.TenantScorelineManage,
|
||||
BackendPermissions.TenantSiteContentManage,
|
||||
BackendPermissions.TenantJobManage
|
||||
];
|
||||
|
||||
private static readonly Regex ScorelineFieldKeyRegex = new("^[A-Za-z][A-Za-z0-9_]{0,63}$", RegexOptions.Compiled);
|
||||
|
||||
private static readonly HashSet<string> SupportedImportTypes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"questions",
|
||||
"vocabulary",
|
||||
"handbook",
|
||||
"scoreline",
|
||||
"videos"
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,8 @@ using Tiku.Infrastructure.Security;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
internal sealed class EducationCatalogManagementService(DirectContentServiceDependencies dependencies)
|
||||
: DirectContentServiceBase(dependencies), IEducationCatalogManagementService
|
||||
{
|
||||
public async Task<CatalogList<School>> GetSchoolsAsync(
|
||||
DirectContentActor actor,
|
||||
@@ -114,4 +115,4 @@ public sealed partial class DirectContentService
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new ContentManagementResult<Major>(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,9 @@ using Tiku.Domain.QuestionBanks;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
internal abstract partial class DirectContentServiceBase
|
||||
{
|
||||
private static QuestionManagementItem ToQuestionItem(Question question, QuestionVersion? version)
|
||||
protected static QuestionManagementItem ToQuestionItem(Question question, QuestionVersion? version)
|
||||
{
|
||||
return new QuestionManagementItem(
|
||||
question.Id,
|
||||
@@ -44,7 +44,7 @@ public sealed partial class DirectContentService
|
||||
question.Status);
|
||||
}
|
||||
|
||||
private static VideoManagementItem ToVideoItem(VideoExplanation item)
|
||||
protected static VideoManagementItem ToVideoItem(VideoExplanation item)
|
||||
{
|
||||
return new VideoManagementItem(
|
||||
item.Id,
|
||||
@@ -63,7 +63,7 @@ public sealed partial class DirectContentService
|
||||
item.Metadata);
|
||||
}
|
||||
|
||||
private static QuestionVideoManagementItem ToQuestionVideoItem(QuestionVideo item)
|
||||
protected static QuestionVideoManagementItem ToQuestionVideoItem(QuestionVideo item)
|
||||
{
|
||||
return new QuestionVideoManagementItem(
|
||||
item.Id,
|
||||
@@ -75,7 +75,7 @@ public sealed partial class DirectContentService
|
||||
item.Metadata);
|
||||
}
|
||||
|
||||
private static OperationContentItem ToOperationItem(Banner item)
|
||||
protected static OperationContentItem ToOperationItem(Banner item)
|
||||
{
|
||||
return new OperationContentItem(
|
||||
item.Id,
|
||||
@@ -101,7 +101,7 @@ public sealed partial class DirectContentService
|
||||
}));
|
||||
}
|
||||
|
||||
private static OperationContentItem ToOperationItem(Faq item)
|
||||
protected static OperationContentItem ToOperationItem(Faq item)
|
||||
{
|
||||
return new OperationContentItem(
|
||||
item.Id,
|
||||
@@ -120,7 +120,7 @@ public sealed partial class DirectContentService
|
||||
JsonDefaults.Object());
|
||||
}
|
||||
|
||||
private static OperationContentItem ToOperationItem(Announcement item)
|
||||
protected static OperationContentItem ToOperationItem(Announcement item)
|
||||
{
|
||||
return new OperationContentItem(
|
||||
item.Id,
|
||||
@@ -143,7 +143,7 @@ public sealed partial class DirectContentService
|
||||
}));
|
||||
}
|
||||
|
||||
private static OperationContentItem ToOperationItem(ExamDate item)
|
||||
protected static OperationContentItem ToOperationItem(ExamDate item)
|
||||
{
|
||||
return new OperationContentItem(
|
||||
item.Id,
|
||||
@@ -162,7 +162,7 @@ public sealed partial class DirectContentService
|
||||
item.Metadata);
|
||||
}
|
||||
|
||||
private static ContentImportJobItem ToJobItem(ContentImportJob job)
|
||||
protected static ContentImportJobItem ToJobItem(ContentImportJob job)
|
||||
{
|
||||
return new ContentImportJobItem(
|
||||
job.Id,
|
||||
@@ -192,7 +192,7 @@ public sealed partial class DirectContentService
|
||||
job.UpdatedAt);
|
||||
}
|
||||
|
||||
private static ContentImportItemModel ToImportItem(ContentImportItem item)
|
||||
protected static ContentImportItemModel ToImportItem(ContentImportItem item)
|
||||
{
|
||||
return new ContentImportItemModel(
|
||||
item.Id,
|
||||
@@ -208,7 +208,7 @@ public sealed partial class DirectContentService
|
||||
item.IssuesCount);
|
||||
}
|
||||
|
||||
private async Task<CurrentDataScope> RequireDataScopeAsync(
|
||||
protected async Task<CurrentDataScope> RequireDataScopeAsync(
|
||||
DirectContentActor actor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -222,7 +222,7 @@ public sealed partial class DirectContentService
|
||||
return access.DataScope;
|
||||
}
|
||||
|
||||
private static void EnsureRegionWriteAllowed(
|
||||
protected static void EnsureRegionWriteAllowed(
|
||||
CurrentDataScope scope,
|
||||
DirectContentActor actor,
|
||||
Guid? currentRegionId,
|
||||
@@ -236,7 +236,7 @@ public sealed partial class DirectContentService
|
||||
throw new ContentManagementException("Content resource was not found.", notFoundCode);
|
||||
}
|
||||
|
||||
private async Task<TEntity?> ResolveByIdOrLegacyAsync<TEntity>(
|
||||
protected async Task<TEntity?> ResolveByIdOrLegacyAsync<TEntity>(
|
||||
DbSet<TEntity> set,
|
||||
Guid tenantId,
|
||||
Guid? id,
|
||||
@@ -256,7 +256,7 @@ public sealed partial class DirectContentService
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task AssertReferenceAsync<TEntity>(
|
||||
protected async Task AssertReferenceAsync<TEntity>(
|
||||
Guid tenantId,
|
||||
Guid? id,
|
||||
string code,
|
||||
@@ -272,7 +272,7 @@ public sealed partial class DirectContentService
|
||||
if (!exists) throw new ContentManagementException("Referenced entity was not found.", code);
|
||||
}
|
||||
|
||||
private async Task AssertImportJobAsync(Guid tenantId, Guid jobId, CancellationToken cancellationToken)
|
||||
protected async Task AssertImportJobAsync(Guid tenantId, Guid jobId, CancellationToken cancellationToken)
|
||||
{
|
||||
var exists = await dbContext.ContentImportJobs.AnyAsync(
|
||||
item => item.TenantId == tenantId && item.Id == jobId,
|
||||
@@ -280,7 +280,7 @@ public sealed partial class DirectContentService
|
||||
if (!exists) throw new ContentManagementException("Import job was not found.", "import_job_not_found");
|
||||
}
|
||||
|
||||
private static string NormalizeOperationKind(string kind)
|
||||
protected static string NormalizeOperationKind(string kind)
|
||||
{
|
||||
var normalized = Normalize(kind)?.ToLowerInvariant();
|
||||
return normalized switch
|
||||
@@ -293,7 +293,7 @@ public sealed partial class DirectContentService
|
||||
};
|
||||
}
|
||||
|
||||
private static ContentImportType ParseImportType(string value)
|
||||
protected static ContentImportType ParseImportType(string value)
|
||||
{
|
||||
return value.ToLowerInvariant() switch
|
||||
{
|
||||
@@ -306,7 +306,7 @@ public sealed partial class DirectContentService
|
||||
};
|
||||
}
|
||||
|
||||
private static TEnum Parse<TEnum>(string? value, TEnum fallback, string code)
|
||||
protected static TEnum Parse<TEnum>(string? value, TEnum fallback, string code)
|
||||
where TEnum : struct
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return fallback;
|
||||
@@ -316,7 +316,7 @@ public sealed partial class DirectContentService
|
||||
throw new ContentManagementException("Enum value is invalid.", code);
|
||||
}
|
||||
|
||||
private static TEnum? ParseNullable<TEnum>(string? value, string code)
|
||||
protected static TEnum? ParseNullable<TEnum>(string? value, string code)
|
||||
where TEnum : struct
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
@@ -326,41 +326,41 @@ public sealed partial class DirectContentService
|
||||
throw new ContentManagementException("Enum value is invalid.", code);
|
||||
}
|
||||
|
||||
private static string? Normalize(string? value)
|
||||
protected static string? Normalize(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
private static int ResolveLimit(int? limit)
|
||||
protected static int ResolveLimit(int? limit)
|
||||
{
|
||||
return !limit.HasValue || limit <= 0 ? DefaultLimit : Math.Min(limit.Value, MaxLimit);
|
||||
}
|
||||
|
||||
private static JsonElement JsonObjectOrDefault(JsonElement value)
|
||||
protected static JsonElement JsonObjectOrDefault(JsonElement value)
|
||||
{
|
||||
return value.ValueKind is JsonValueKind.Object ? value : JsonDefaults.Object();
|
||||
}
|
||||
|
||||
private static JsonElement JsonArrayOrDefault(JsonElement value)
|
||||
protected static JsonElement JsonArrayOrDefault(JsonElement value)
|
||||
{
|
||||
return value.ValueKind is JsonValueKind.Array ? value : JsonDefaults.Array();
|
||||
}
|
||||
|
||||
private static JsonElement GetElement(JsonElement payload, string name, JsonElement fallback)
|
||||
protected 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)
|
||||
protected 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)
|
||||
protected static int? GetInt(JsonElement payload, string name)
|
||||
{
|
||||
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
|
||||
|
||||
@@ -371,7 +371,7 @@ public sealed partial class DirectContentService
|
||||
: null;
|
||||
}
|
||||
|
||||
private static Guid? GetGuid(JsonElement payload, string name)
|
||||
protected static Guid? GetGuid(JsonElement payload, string name)
|
||||
{
|
||||
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
|
||||
|
||||
@@ -381,7 +381,7 @@ public sealed partial class DirectContentService
|
||||
: null;
|
||||
}
|
||||
|
||||
private static bool? GetBool(JsonElement payload, string name)
|
||||
protected static bool? GetBool(JsonElement payload, string name)
|
||||
{
|
||||
if (payload.ValueKind != JsonValueKind.Object || !payload.TryGetProperty(name, out var value)) return null;
|
||||
|
||||
@@ -393,4 +393,4 @@ public sealed partial class DirectContentService
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,191 +12,9 @@ using Tiku.Domain.QuestionBanks;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
internal abstract partial class DirectContentServiceBase
|
||||
{
|
||||
private async Task<SimpleImportResult> CreateImportJobAsync(
|
||||
DirectContentActor actor,
|
||||
SimpleImportCommand command,
|
||||
bool execute,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!SupportedImportTypes.Contains(command.ImportType))
|
||||
throw new ContentManagementException("Import type is invalid.", "import_type_invalid");
|
||||
|
||||
var importType = ParseImportType(command.ImportType);
|
||||
var sourceFormat = Parse(command.SourceFormat, ImportSourceFormat.Json, "import_source_format_invalid");
|
||||
var items = command.Items
|
||||
.Select(item => item.ValueKind == JsonValueKind.Undefined ? JsonDefaults.Object() : item).ToArray();
|
||||
var job = new ContentImportJob
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
CreatedBy = actor.UserId,
|
||||
TargetRegionId = command.RegionId,
|
||||
TargetSubjectId = command.SubjectId,
|
||||
TargetCategoryId = command.CategoryId,
|
||||
TargetContentNodeId = command.ContentNodeId,
|
||||
TargetQuestionBankId = command.QuestionBankId,
|
||||
ImportType = importType,
|
||||
SourceFormat = sourceFormat,
|
||||
Status = execute ? ContentImportStatus.Completed : ContentImportStatus.Preview,
|
||||
SourceName = Normalize(command.SourceName),
|
||||
DryRun = command.DryRun,
|
||||
TotalCount = items.Length,
|
||||
ValidCount = items.Length,
|
||||
RawPayload = JsonSerializer.SerializeToElement(items),
|
||||
NormalizedPayload = JsonSerializer.SerializeToElement(items),
|
||||
StartedAt = execute ? DateTimeOffset.UtcNow : null,
|
||||
FinishedAt = execute ? DateTimeOffset.UtcNow : null
|
||||
};
|
||||
dbContext.ContentImportJobs.Add(job);
|
||||
|
||||
var importItems = new List<ContentImportItem>();
|
||||
var rowNo = 1;
|
||||
foreach (var payload in items)
|
||||
{
|
||||
var importItem = new ContentImportItem
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
JobId = job.Id,
|
||||
RowNo = rowNo++,
|
||||
ExternalId = GetString(payload, "legacyId") ?? GetString(payload, "id"),
|
||||
Status = execute ? ContentImportItemStatus.Inserted : ContentImportItemStatus.Valid,
|
||||
SourcePayload = payload,
|
||||
NormalizedPayload = payload
|
||||
};
|
||||
|
||||
if (execute)
|
||||
{
|
||||
var target = await WriteImportedItemAsync(actor, command, payload, cancellationToken);
|
||||
importItem.TargetType = target.TargetType;
|
||||
importItem.TargetId = target.TargetId;
|
||||
job.InsertedCount++;
|
||||
}
|
||||
|
||||
importItems.Add(importItem);
|
||||
}
|
||||
|
||||
dbContext.ContentImportItems.AddRange(importItems);
|
||||
job.Summary = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
mode = execute ? "execute" : "preview",
|
||||
supportedTypes = SupportedImportTypes,
|
||||
note = "Synchronous direct migration import skeleton; async worker will be introduced later."
|
||||
});
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new SimpleImportResult(
|
||||
ToJobItem(job),
|
||||
importItems.Select(ToImportItem).ToArray(),
|
||||
[]);
|
||||
}
|
||||
|
||||
private async Task<(string TargetType, Guid TargetId)> WriteImportedItemAsync(
|
||||
DirectContentActor actor,
|
||||
SimpleImportCommand command,
|
||||
JsonElement payload,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
switch (command.ImportType.ToLowerInvariant())
|
||||
{
|
||||
case "questions":
|
||||
var result = await CreateQuestionAsync(actor, new QuestionWriteCommand(
|
||||
null,
|
||||
command.QuestionBankId,
|
||||
command.SubjectId,
|
||||
command.CategoryId,
|
||||
null,
|
||||
command.EntryId,
|
||||
command.ContentNodeId,
|
||||
command.CollectionId,
|
||||
GetString(payload, "legacyId"),
|
||||
GetString(payload, "type") ?? "choice",
|
||||
GetString(payload, "typeLabel"),
|
||||
GetInt(payload, "difficulty"),
|
||||
GetElement(payload, "tags", JsonDefaults.Array()),
|
||||
GetString(payload, "content") ?? GetString(payload, "title"),
|
||||
GetElement(payload, "options", JsonDefaults.Array()),
|
||||
GetInt(payload, "correctOptionIndex"),
|
||||
GetElement(payload, "correctOptionIndices", JsonDefaults.Array()),
|
||||
GetString(payload, "answerText") ?? GetString(payload, "answer"),
|
||||
GetString(payload, "explanation"),
|
||||
GetElement(payload, "subQuestions", JsonDefaults.Array()),
|
||||
GetString(payload, "codeLang"),
|
||||
GetString(payload, "codeTemplate"),
|
||||
GetString(payload, "mediaUrl"),
|
||||
"Published",
|
||||
GetElement(payload, "examMarkers", JsonDefaults.Object()),
|
||||
GetString(payload, "sourceHash"),
|
||||
true), cancellationToken);
|
||||
return ("question", result.Item.Id);
|
||||
case "vocabulary":
|
||||
var word = await UpsertVocabularyWordAsync(actor, new VocabularyWordCommand(
|
||||
null,
|
||||
null,
|
||||
command.EntryId,
|
||||
command.ContentNodeId,
|
||||
GetString(payload, "legacyId"),
|
||||
GetString(payload, "word") ?? GetString(payload, "name") ?? "未命名单词",
|
||||
GetString(payload, "phonetic"),
|
||||
GetString(payload, "meaning"),
|
||||
GetString(payload, "example"),
|
||||
GetString(payload, "exampleTranslation"),
|
||||
GetInt(payload, "difficulty"),
|
||||
GetElement(payload, "tags", JsonDefaults.Array()),
|
||||
GetInt(payload, "order"),
|
||||
true,
|
||||
GetElement(payload, "metadata", JsonDefaults.Object())), cancellationToken);
|
||||
return ("vocabulary_word", word.Item.Id);
|
||||
case "handbook":
|
||||
var entry = await UpsertHandbookEntryAsync(actor, new HandbookEntryCommand(
|
||||
null,
|
||||
null,
|
||||
command.EntryId,
|
||||
command.ContentNodeId,
|
||||
GetString(payload, "legacyId"),
|
||||
GetString(payload, "title") ?? GetString(payload, "name") ?? "未命名条目",
|
||||
GetString(payload, "summary"),
|
||||
GetString(payload, "content"),
|
||||
GetElement(payload, "tags", JsonDefaults.Array()),
|
||||
GetInt(payload, "order"),
|
||||
true,
|
||||
GetElement(payload, "metadata", JsonDefaults.Object())), cancellationToken);
|
||||
return ("handbook_entry", entry.Item.Id);
|
||||
case "scoreline":
|
||||
var scoreline = await UpsertScorelineRecordAsync(actor, new ScorelineRecordCommand(
|
||||
null,
|
||||
command.RegionId,
|
||||
GetGuid(payload, "schoolId"),
|
||||
GetGuid(payload, "majorId"),
|
||||
GetString(payload, "legacyId"),
|
||||
GetInt(payload, "year") ?? DateTimeOffset.UtcNow.Year,
|
||||
GetString(payload, "schoolName"),
|
||||
GetString(payload, "majorName"),
|
||||
GetElement(payload, "fieldValues", payload)), cancellationToken);
|
||||
return ("scoreline_record", scoreline.Item.Id);
|
||||
case "videos":
|
||||
var video = await UpsertVideoAsync(actor, new VideoExplanationCommand(
|
||||
null,
|
||||
command.SubjectId,
|
||||
GetString(payload, "legacyId"),
|
||||
GetString(payload, "title") ?? "未命名视频",
|
||||
GetString(payload, "description"),
|
||||
GetString(payload, "videoUrl") ?? GetString(payload, "url"),
|
||||
GetString(payload, "thumbnailUrl"),
|
||||
GetInt(payload, "durationSeconds"),
|
||||
GetElement(payload, "knowledgeTags", JsonDefaults.Array()),
|
||||
GetBool(payload, "isGeneral"),
|
||||
GetInt(payload, "difficulty"),
|
||||
GetInt(payload, "order"),
|
||||
true,
|
||||
GetElement(payload, "metadata", JsonDefaults.Object())), cancellationToken);
|
||||
return ("video_explanation", video.Item.Id);
|
||||
default:
|
||||
throw new ContentManagementException("Import type is invalid.", "import_type_invalid");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<Banner> UpsertBannerAsync(DirectContentActor actor, OperationContentCommand command,
|
||||
protected async Task<Banner> UpsertBannerAsync(DirectContentActor actor, OperationContentCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);
|
||||
@@ -221,7 +39,7 @@ public sealed partial class DirectContentService
|
||||
return item;
|
||||
}
|
||||
|
||||
private async Task<Faq> UpsertFaqAsync(DirectContentActor actor, OperationContentCommand command,
|
||||
protected async Task<Faq> UpsertFaqAsync(DirectContentActor actor, OperationContentCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await AssertReferenceAsync<Region>(actor.TenantId, command.RegionId, "region_not_found", cancellationToken);
|
||||
@@ -241,7 +59,7 @@ public sealed partial class DirectContentService
|
||||
return item;
|
||||
}
|
||||
|
||||
private async Task<Announcement> UpsertAnnouncementAsync(DirectContentActor actor, OperationContentCommand command,
|
||||
protected async Task<Announcement> UpsertAnnouncementAsync(DirectContentActor actor, OperationContentCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var item = await ResolveByIdOrLegacyAsync(dbContext.Announcements, actor.TenantId, command.Id, command.LegacyId,
|
||||
@@ -260,7 +78,7 @@ public sealed partial class DirectContentService
|
||||
return item;
|
||||
}
|
||||
|
||||
private async Task<ExamDate> UpsertExamDateAsync(DirectContentActor actor, OperationContentCommand command,
|
||||
protected async Task<ExamDate> UpsertExamDateAsync(DirectContentActor actor, OperationContentCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(command.ExamName);
|
||||
@@ -286,7 +104,7 @@ public sealed partial class DirectContentService
|
||||
return item;
|
||||
}
|
||||
|
||||
private static void ApplyQuestion(Question question, QuestionWriteCommand command)
|
||||
protected static void ApplyQuestion(Question question, QuestionWriteCommand command)
|
||||
{
|
||||
question.QuestionBankId = command.QuestionBankId;
|
||||
question.SubjectId = command.SubjectId;
|
||||
@@ -305,7 +123,7 @@ public sealed partial class DirectContentService
|
||||
question.Status = Parse(command.Status, QuestionStatus.Published, "question_status_invalid");
|
||||
}
|
||||
|
||||
private static void ValidateQuestionForPublication(QuestionWriteCommand command)
|
||||
protected static void ValidateQuestionForPublication(QuestionWriteCommand command)
|
||||
{
|
||||
var status = Parse(command.Status, QuestionStatus.Published, "question_status_invalid");
|
||||
if (status != QuestionStatus.Published) return;
|
||||
@@ -321,7 +139,7 @@ public sealed partial class DirectContentService
|
||||
"question_grading_rule_invalid");
|
||||
}
|
||||
|
||||
private static QuestionVersion BuildQuestionVersion(
|
||||
protected static QuestionVersion BuildQuestionVersion(
|
||||
DirectContentActor actor,
|
||||
Guid questionId,
|
||||
int versionNo,
|
||||
@@ -338,7 +156,7 @@ public sealed partial class DirectContentService
|
||||
return version;
|
||||
}
|
||||
|
||||
private static void ApplyQuestionVersion(QuestionVersion version, QuestionWriteCommand command)
|
||||
protected static void ApplyQuestionVersion(QuestionVersion version, QuestionWriteCommand command)
|
||||
{
|
||||
version.Content = Normalize(command.Content);
|
||||
version.Options = JsonArrayOrDefault(command.Options);
|
||||
@@ -352,7 +170,7 @@ public sealed partial class DirectContentService
|
||||
version.SourceHash = Normalize(command.SourceHash);
|
||||
}
|
||||
|
||||
private async Task AssertQuestionReferencesAsync(Guid tenantId, QuestionWriteCommand command,
|
||||
protected async Task AssertQuestionReferencesAsync(Guid tenantId, QuestionWriteCommand command,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await AssertReferenceAsync<QuestionBank>(tenantId, command.QuestionBankId, "question_bank_not_found",
|
||||
@@ -366,7 +184,7 @@ public sealed partial class DirectContentService
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task SyncPrimaryCollectionItemAsync(
|
||||
protected async Task SyncPrimaryCollectionItemAsync(
|
||||
DirectContentActor actor,
|
||||
Question question,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -411,7 +229,7 @@ public sealed partial class DirectContentService
|
||||
collection.UpdatedBy = actor.UserId;
|
||||
}
|
||||
|
||||
private async Task<(Guid? EntryId, Guid? ContentNodeId)> ResolveVocabularyNavigationAsync(
|
||||
protected async Task<(Guid? EntryId, Guid? ContentNodeId)> ResolveVocabularyNavigationAsync(
|
||||
Guid tenantId,
|
||||
Guid? unitId,
|
||||
Guid? entryId,
|
||||
@@ -429,7 +247,7 @@ public sealed partial class DirectContentService
|
||||
return (entryId ?? unit.EntryId, contentNodeId ?? unit.ContentNodeId);
|
||||
}
|
||||
|
||||
private async Task<(Guid? EntryId, Guid? ContentNodeId)> ResolveHandbookSubjectNavigationAsync(
|
||||
protected async Task<(Guid? EntryId, Guid? ContentNodeId)> ResolveHandbookSubjectNavigationAsync(
|
||||
Guid tenantId,
|
||||
Guid? subjectId,
|
||||
Guid? entryId,
|
||||
@@ -447,7 +265,7 @@ public sealed partial class DirectContentService
|
||||
return (entryId ?? subject.EntryId, contentNodeId ?? subject.ContentNodeId);
|
||||
}
|
||||
|
||||
private async Task<(Guid? EntryId, Guid? ContentNodeId)> ResolveHandbookChapterNavigationAsync(
|
||||
protected async Task<(Guid? EntryId, Guid? ContentNodeId)> ResolveHandbookChapterNavigationAsync(
|
||||
Guid tenantId,
|
||||
Guid? chapterId,
|
||||
Guid? entryId,
|
||||
@@ -464,4 +282,4 @@ public sealed partial class DirectContentService
|
||||
|
||||
return (entryId ?? chapter.EntryId, contentNodeId ?? chapter.ContentNodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.QuestionBanks;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
internal sealed record DirectContentServiceDependencies(
|
||||
TikuDbContext DbContext,
|
||||
IQuestionReferenceService QuestionReferenceService,
|
||||
ICurrentAccessContext CurrentAccessContext,
|
||||
IFeatureAccessService FeatureAccessService);
|
||||
|
||||
internal abstract partial class DirectContentServiceBase(DirectContentServiceDependencies dependencies)
|
||||
{
|
||||
protected TikuDbContext dbContext { get; } = dependencies.DbContext;
|
||||
protected IQuestionReferenceService questionReferenceService { get; } = dependencies.QuestionReferenceService;
|
||||
protected ICurrentAccessContext currentAccessContext { get; } = dependencies.CurrentAccessContext;
|
||||
protected IFeatureAccessService featureAccessService { get; } = dependencies.FeatureAccessService;
|
||||
|
||||
protected const int DefaultLimit = 100;
|
||||
protected const int MaxLimit = 1000;
|
||||
|
||||
protected static readonly string[] ContentPermissions =
|
||||
[
|
||||
BackendPermissions.TenantContentManage,
|
||||
BackendPermissions.TenantVocabularyManage,
|
||||
BackendPermissions.TenantHandbookManage,
|
||||
BackendPermissions.TenantVideoManage,
|
||||
BackendPermissions.TenantScorelineManage,
|
||||
BackendPermissions.TenantSiteContentManage,
|
||||
BackendPermissions.TenantJobManage
|
||||
];
|
||||
|
||||
protected static readonly Regex ScorelineFieldKeyRegex = new("^[A-Za-z][A-Za-z0-9_]{0,63}$", RegexOptions.Compiled);
|
||||
|
||||
protected static readonly HashSet<string> SupportedImportTypes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"questions",
|
||||
"vocabulary",
|
||||
"handbook",
|
||||
"scoreline",
|
||||
"videos"
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,8 @@ using Tiku.Infrastructure.Security;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
internal sealed class HandbookManagementService(DirectContentServiceDependencies dependencies)
|
||||
: DirectContentServiceBase(dependencies), IHandbookManagementService
|
||||
{
|
||||
public async Task<CatalogList<HandbookSubject>> GetHandbookSubjectsAsync(
|
||||
DirectContentActor actor,
|
||||
@@ -221,4 +222,4 @@ public sealed partial class DirectContentService
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new ContentManagementResult<HandbookEntry>(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using Tiku.Infrastructure.Jobs;
|
||||
|
||||
namespace Tiku.Infrastructure.Content.Imports;
|
||||
|
||||
internal sealed class ContentImportJobHandler(IDirectContentService directContentService) : IBackgroundJobHandler
|
||||
internal sealed class ContentImportJobHandler(IContentImportService contentImportService) : IBackgroundJobHandler
|
||||
{
|
||||
public string JobType => "content_import";
|
||||
|
||||
@@ -30,7 +30,7 @@ internal sealed class ContentImportJobHandler(IDirectContentService directConten
|
||||
BackgroundJobPayload.GetGuid(context.Payload, "collectionId"),
|
||||
BackgroundJobPayload.GetArray(context.Payload, "items"),
|
||||
false);
|
||||
var result = await directContentService.ExecuteImportAsync(
|
||||
var result = await contentImportService.ExecuteImportAsync(
|
||||
new DirectContentActor(context.TenantId, createdBy),
|
||||
command,
|
||||
cancellationToken);
|
||||
|
||||
339
Tiku.Infrastructure/Content/Imports/ContentImportService.cs
Normal file
339
Tiku.Infrastructure/Content/Imports/ContentImportService.cs
Normal file
@@ -0,0 +1,339 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Common;
|
||||
using Tiku.Domain.Content;
|
||||
using Tiku.Domain.Learning;
|
||||
using Tiku.Domain.Operations;
|
||||
using Tiku.Domain.QuestionBanks;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
internal sealed class ContentImportService(
|
||||
DirectContentServiceDependencies dependencies,
|
||||
IQuestionManagementService questionService,
|
||||
IVocabularyManagementService vocabularyService,
|
||||
IHandbookManagementService handbookService,
|
||||
IScorelineManagementService scorelineService,
|
||||
IVideoManagementService videoService)
|
||||
: DirectContentServiceBase(dependencies), IContentImportService
|
||||
{
|
||||
public Task<SimpleImportResult> PreviewImportAsync(
|
||||
DirectContentActor actor,
|
||||
SimpleImportCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return CreateImportJobAsync(actor, command with { DryRun = true }, false, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<SimpleImportResult> ExecuteImportAsync(
|
||||
DirectContentActor actor,
|
||||
SimpleImportCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return CreateImportJobAsync(actor, command with { DryRun = false }, true, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<ContentImportJobDetail> GetImportJobAsync(
|
||||
DirectContentActor actor,
|
||||
Guid jobId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var job = await dbContext.ContentImportJobs.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId && item.Id == jobId)
|
||||
.Select(item => ToJobItem(item))
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
if (job is null) throw new ContentManagementException("Import job was not found.", "import_job_not_found");
|
||||
|
||||
var items = await dbContext.ContentImportItems.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId && item.JobId == jobId)
|
||||
.OrderBy(item => item.RowNo)
|
||||
.Take(MaxLimit)
|
||||
.Select(item => ToImportItem(item))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var issues = await dbContext.ContentImportIssues.AsNoTracking()
|
||||
.Where(issue => issue.TenantId == actor.TenantId && issue.JobId == jobId)
|
||||
.OrderBy(issue => issue.RowNo)
|
||||
.ThenBy(issue => issue.CreatedAt)
|
||||
.Take(MaxLimit)
|
||||
.Select(issue => new ContentImportIssueModel(
|
||||
issue.Id,
|
||||
issue.JobId,
|
||||
issue.ItemId,
|
||||
issue.RowNo,
|
||||
issue.Severity,
|
||||
issue.Code,
|
||||
issue.FieldPath,
|
||||
issue.Message,
|
||||
issue.Details))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return new ContentImportJobDetail(job, items, issues);
|
||||
}
|
||||
|
||||
public async Task<CatalogList<ContentImportIssueModel>> GetImportIssuesAsync(
|
||||
DirectContentActor actor,
|
||||
Guid jobId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertImportJobAsync(actor.TenantId, jobId, cancellationToken);
|
||||
var issues = await dbContext.ContentImportIssues.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId && item.JobId == jobId)
|
||||
.OrderBy(item => item.RowNo)
|
||||
.ThenBy(item => item.CreatedAt)
|
||||
.Take(MaxLimit)
|
||||
.Select(item => new ContentImportIssueModel(
|
||||
item.Id,
|
||||
item.JobId,
|
||||
item.ItemId,
|
||||
item.RowNo,
|
||||
item.Severity,
|
||||
item.Code,
|
||||
item.FieldPath,
|
||||
item.Message,
|
||||
item.Details))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return new CatalogList<ContentImportIssueModel>(issues);
|
||||
}
|
||||
|
||||
public async Task<ImportPostCheckResult> RunImportPostCheckAsync(
|
||||
DirectContentActor actor,
|
||||
Guid jobId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var job = await dbContext.ContentImportJobs.SingleOrDefaultAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == jobId,
|
||||
cancellationToken);
|
||||
if (job is null) throw new ContentManagementException("Import job was not found.", "import_job_not_found");
|
||||
|
||||
var counts = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
job.TotalCount,
|
||||
job.ValidCount,
|
||||
job.ErrorCount,
|
||||
job.WarningCount,
|
||||
job.InsertedCount,
|
||||
job.UpdatedCount,
|
||||
job.SkippedCount
|
||||
});
|
||||
job.Summary = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
postCheck = new
|
||||
{
|
||||
status = job.ErrorCount == 0 ? "passed" : "warning",
|
||||
checkedAt = DateTimeOffset.UtcNow,
|
||||
counts
|
||||
}
|
||||
});
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new ImportPostCheckResult(job.Id, job.ErrorCount == 0 ? "passed" : "warning", counts, []);
|
||||
}
|
||||
|
||||
public async Task<ImportPostCheckResult> GetImportPostCheckAsync(
|
||||
DirectContentActor actor,
|
||||
Guid jobId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var job = await dbContext.ContentImportJobs.AsNoTracking().SingleOrDefaultAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == jobId,
|
||||
cancellationToken);
|
||||
if (job is null) throw new ContentManagementException("Import job was not found.", "import_job_not_found");
|
||||
|
||||
var issues = await GetImportIssuesAsync(actor, jobId, cancellationToken);
|
||||
var counts = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
job.TotalCount,
|
||||
job.ValidCount,
|
||||
job.ErrorCount,
|
||||
job.WarningCount,
|
||||
job.InsertedCount,
|
||||
job.UpdatedCount,
|
||||
job.SkippedCount
|
||||
});
|
||||
return new ImportPostCheckResult(job.Id, job.ErrorCount == 0 ? "passed" : "warning", counts, issues.Items);
|
||||
}
|
||||
private async Task<SimpleImportResult> CreateImportJobAsync(
|
||||
DirectContentActor actor,
|
||||
SimpleImportCommand command,
|
||||
bool execute,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (!SupportedImportTypes.Contains(command.ImportType))
|
||||
throw new ContentManagementException("Import type is invalid.", "import_type_invalid");
|
||||
|
||||
var importType = ParseImportType(command.ImportType);
|
||||
var sourceFormat = Parse(command.SourceFormat, ImportSourceFormat.Json, "import_source_format_invalid");
|
||||
var items = command.Items
|
||||
.Select(item => item.ValueKind == JsonValueKind.Undefined ? JsonDefaults.Object() : item).ToArray();
|
||||
var job = new ContentImportJob
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
CreatedBy = actor.UserId,
|
||||
TargetRegionId = command.RegionId,
|
||||
TargetSubjectId = command.SubjectId,
|
||||
TargetCategoryId = command.CategoryId,
|
||||
TargetContentNodeId = command.ContentNodeId,
|
||||
TargetQuestionBankId = command.QuestionBankId,
|
||||
ImportType = importType,
|
||||
SourceFormat = sourceFormat,
|
||||
Status = execute ? ContentImportStatus.Completed : ContentImportStatus.Preview,
|
||||
SourceName = Normalize(command.SourceName),
|
||||
DryRun = command.DryRun,
|
||||
TotalCount = items.Length,
|
||||
ValidCount = items.Length,
|
||||
RawPayload = JsonSerializer.SerializeToElement(items),
|
||||
NormalizedPayload = JsonSerializer.SerializeToElement(items),
|
||||
StartedAt = execute ? DateTimeOffset.UtcNow : null,
|
||||
FinishedAt = execute ? DateTimeOffset.UtcNow : null
|
||||
};
|
||||
dbContext.ContentImportJobs.Add(job);
|
||||
|
||||
var importItems = new List<ContentImportItem>();
|
||||
var rowNo = 1;
|
||||
foreach (var payload in items)
|
||||
{
|
||||
var importItem = new ContentImportItem
|
||||
{
|
||||
TenantId = actor.TenantId,
|
||||
JobId = job.Id,
|
||||
RowNo = rowNo++,
|
||||
ExternalId = GetString(payload, "legacyId") ?? GetString(payload, "id"),
|
||||
Status = execute ? ContentImportItemStatus.Inserted : ContentImportItemStatus.Valid,
|
||||
SourcePayload = payload,
|
||||
NormalizedPayload = payload
|
||||
};
|
||||
|
||||
if (execute)
|
||||
{
|
||||
var target = await WriteImportedItemAsync(actor, command, payload, cancellationToken);
|
||||
importItem.TargetType = target.TargetType;
|
||||
importItem.TargetId = target.TargetId;
|
||||
job.InsertedCount++;
|
||||
}
|
||||
|
||||
importItems.Add(importItem);
|
||||
}
|
||||
|
||||
dbContext.ContentImportItems.AddRange(importItems);
|
||||
job.Summary = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
mode = execute ? "execute" : "preview",
|
||||
supportedTypes = SupportedImportTypes,
|
||||
note = "Synchronous direct migration import skeleton; async worker will be introduced later."
|
||||
});
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new SimpleImportResult(
|
||||
ToJobItem(job),
|
||||
importItems.Select(ToImportItem).ToArray(),
|
||||
[]);
|
||||
}
|
||||
|
||||
private async Task<(string TargetType, Guid TargetId)> WriteImportedItemAsync(
|
||||
DirectContentActor actor,
|
||||
SimpleImportCommand command,
|
||||
JsonElement payload,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
switch (command.ImportType.ToLowerInvariant())
|
||||
{
|
||||
case "questions":
|
||||
var result = await questionService.CreateQuestionAsync(actor, new QuestionWriteCommand(
|
||||
null,
|
||||
command.QuestionBankId,
|
||||
command.SubjectId,
|
||||
command.CategoryId,
|
||||
null,
|
||||
command.EntryId,
|
||||
command.ContentNodeId,
|
||||
command.CollectionId,
|
||||
GetString(payload, "legacyId"),
|
||||
GetString(payload, "type") ?? "choice",
|
||||
GetString(payload, "typeLabel"),
|
||||
GetInt(payload, "difficulty"),
|
||||
GetElement(payload, "tags", JsonDefaults.Array()),
|
||||
GetString(payload, "content") ?? GetString(payload, "title"),
|
||||
GetElement(payload, "options", JsonDefaults.Array()),
|
||||
GetInt(payload, "correctOptionIndex"),
|
||||
GetElement(payload, "correctOptionIndices", JsonDefaults.Array()),
|
||||
GetString(payload, "answerText") ?? GetString(payload, "answer"),
|
||||
GetString(payload, "explanation"),
|
||||
GetElement(payload, "subQuestions", JsonDefaults.Array()),
|
||||
GetString(payload, "codeLang"),
|
||||
GetString(payload, "codeTemplate"),
|
||||
GetString(payload, "mediaUrl"),
|
||||
"Published",
|
||||
GetElement(payload, "examMarkers", JsonDefaults.Object()),
|
||||
GetString(payload, "sourceHash"),
|
||||
true), cancellationToken);
|
||||
return ("question", result.Item.Id);
|
||||
case "vocabulary":
|
||||
var word = await vocabularyService.UpsertVocabularyWordAsync(actor, new VocabularyWordCommand(
|
||||
null,
|
||||
null,
|
||||
command.EntryId,
|
||||
command.ContentNodeId,
|
||||
GetString(payload, "legacyId"),
|
||||
GetString(payload, "word") ?? GetString(payload, "name") ?? "未命名单词",
|
||||
GetString(payload, "phonetic"),
|
||||
GetString(payload, "meaning"),
|
||||
GetString(payload, "example"),
|
||||
GetString(payload, "exampleTranslation"),
|
||||
GetInt(payload, "difficulty"),
|
||||
GetElement(payload, "tags", JsonDefaults.Array()),
|
||||
GetInt(payload, "order"),
|
||||
true,
|
||||
GetElement(payload, "metadata", JsonDefaults.Object())), cancellationToken);
|
||||
return ("vocabulary_word", word.Item.Id);
|
||||
case "handbook":
|
||||
var entry = await handbookService.UpsertHandbookEntryAsync(actor, new HandbookEntryCommand(
|
||||
null,
|
||||
null,
|
||||
command.EntryId,
|
||||
command.ContentNodeId,
|
||||
GetString(payload, "legacyId"),
|
||||
GetString(payload, "title") ?? GetString(payload, "name") ?? "未命名条目",
|
||||
GetString(payload, "summary"),
|
||||
GetString(payload, "content"),
|
||||
GetElement(payload, "tags", JsonDefaults.Array()),
|
||||
GetInt(payload, "order"),
|
||||
true,
|
||||
GetElement(payload, "metadata", JsonDefaults.Object())), cancellationToken);
|
||||
return ("handbook_entry", entry.Item.Id);
|
||||
case "scoreline":
|
||||
var scoreline = await scorelineService.UpsertScorelineRecordAsync(actor, new ScorelineRecordCommand(
|
||||
null,
|
||||
command.RegionId,
|
||||
GetGuid(payload, "schoolId"),
|
||||
GetGuid(payload, "majorId"),
|
||||
GetString(payload, "legacyId"),
|
||||
GetInt(payload, "year") ?? DateTimeOffset.UtcNow.Year,
|
||||
GetString(payload, "schoolName"),
|
||||
GetString(payload, "majorName"),
|
||||
GetElement(payload, "fieldValues", payload)), cancellationToken);
|
||||
return ("scoreline_record", scoreline.Item.Id);
|
||||
case "videos":
|
||||
var video = await videoService.UpsertVideoAsync(actor, new VideoExplanationCommand(
|
||||
null,
|
||||
command.SubjectId,
|
||||
GetString(payload, "legacyId"),
|
||||
GetString(payload, "title") ?? "未命名视频",
|
||||
GetString(payload, "description"),
|
||||
GetString(payload, "videoUrl") ?? GetString(payload, "url"),
|
||||
GetString(payload, "thumbnailUrl"),
|
||||
GetInt(payload, "durationSeconds"),
|
||||
GetElement(payload, "knowledgeTags", JsonDefaults.Array()),
|
||||
GetBool(payload, "isGeneral"),
|
||||
GetInt(payload, "difficulty"),
|
||||
GetInt(payload, "order"),
|
||||
true,
|
||||
GetElement(payload, "metadata", JsonDefaults.Object())), cancellationToken);
|
||||
return ("video_explanation", video.Item.Id);
|
||||
default:
|
||||
throw new ContentManagementException("Import type is invalid.", "import_type_invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
{
|
||||
public Task<SimpleImportResult> PreviewImportAsync(
|
||||
DirectContentActor actor,
|
||||
SimpleImportCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return CreateImportJobAsync(actor, command with { DryRun = true }, false, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<SimpleImportResult> ExecuteImportAsync(
|
||||
DirectContentActor actor,
|
||||
SimpleImportCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return CreateImportJobAsync(actor, command with { DryRun = false }, true, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<ContentImportJobDetail> GetImportJobAsync(
|
||||
DirectContentActor actor,
|
||||
Guid jobId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var job = await dbContext.ContentImportJobs.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId && item.Id == jobId)
|
||||
.Select(item => ToJobItem(item))
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
if (job is null) throw new ContentManagementException("Import job was not found.", "import_job_not_found");
|
||||
|
||||
var items = await dbContext.ContentImportItems.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId && item.JobId == jobId)
|
||||
.OrderBy(item => item.RowNo)
|
||||
.Take(MaxLimit)
|
||||
.Select(item => ToImportItem(item))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
var issues = await dbContext.ContentImportIssues.AsNoTracking()
|
||||
.Where(issue => issue.TenantId == actor.TenantId && issue.JobId == jobId)
|
||||
.OrderBy(issue => issue.RowNo)
|
||||
.ThenBy(issue => issue.CreatedAt)
|
||||
.Take(MaxLimit)
|
||||
.Select(issue => new ContentImportIssueModel(
|
||||
issue.Id,
|
||||
issue.JobId,
|
||||
issue.ItemId,
|
||||
issue.RowNo,
|
||||
issue.Severity,
|
||||
issue.Code,
|
||||
issue.FieldPath,
|
||||
issue.Message,
|
||||
issue.Details))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
|
||||
return new ContentImportJobDetail(job, items, issues);
|
||||
}
|
||||
|
||||
public async Task<CatalogList<ContentImportIssueModel>> GetImportIssuesAsync(
|
||||
DirectContentActor actor,
|
||||
Guid jobId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertImportJobAsync(actor.TenantId, jobId, cancellationToken);
|
||||
var issues = await dbContext.ContentImportIssues.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId && item.JobId == jobId)
|
||||
.OrderBy(item => item.RowNo)
|
||||
.ThenBy(item => item.CreatedAt)
|
||||
.Take(MaxLimit)
|
||||
.Select(item => new ContentImportIssueModel(
|
||||
item.Id,
|
||||
item.JobId,
|
||||
item.ItemId,
|
||||
item.RowNo,
|
||||
item.Severity,
|
||||
item.Code,
|
||||
item.FieldPath,
|
||||
item.Message,
|
||||
item.Details))
|
||||
.ToArrayAsync(cancellationToken);
|
||||
return new CatalogList<ContentImportIssueModel>(issues);
|
||||
}
|
||||
|
||||
public async Task<ImportPostCheckResult> RunImportPostCheckAsync(
|
||||
DirectContentActor actor,
|
||||
Guid jobId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var job = await dbContext.ContentImportJobs.SingleOrDefaultAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == jobId,
|
||||
cancellationToken);
|
||||
if (job is null) throw new ContentManagementException("Import job was not found.", "import_job_not_found");
|
||||
|
||||
var counts = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
job.TotalCount,
|
||||
job.ValidCount,
|
||||
job.ErrorCount,
|
||||
job.WarningCount,
|
||||
job.InsertedCount,
|
||||
job.UpdatedCount,
|
||||
job.SkippedCount
|
||||
});
|
||||
job.Summary = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
postCheck = new
|
||||
{
|
||||
status = job.ErrorCount == 0 ? "passed" : "warning",
|
||||
checkedAt = DateTimeOffset.UtcNow,
|
||||
counts
|
||||
}
|
||||
});
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new ImportPostCheckResult(job.Id, job.ErrorCount == 0 ? "passed" : "warning", counts, []);
|
||||
}
|
||||
|
||||
public async Task<ImportPostCheckResult> GetImportPostCheckAsync(
|
||||
DirectContentActor actor,
|
||||
Guid jobId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var job = await dbContext.ContentImportJobs.AsNoTracking().SingleOrDefaultAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == jobId,
|
||||
cancellationToken);
|
||||
if (job is null) throw new ContentManagementException("Import job was not found.", "import_job_not_found");
|
||||
|
||||
var issues = await GetImportIssuesAsync(actor, jobId, cancellationToken);
|
||||
var counts = JsonSerializer.SerializeToElement(new
|
||||
{
|
||||
job.TotalCount,
|
||||
job.ValidCount,
|
||||
job.ErrorCount,
|
||||
job.WarningCount,
|
||||
job.InsertedCount,
|
||||
job.UpdatedCount,
|
||||
job.SkippedCount
|
||||
});
|
||||
return new ImportPostCheckResult(job.Id, job.ErrorCount == 0 ? "passed" : "warning", counts, issues.Items);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,8 @@ using Tiku.Application.Content;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
internal sealed class OperationContentManagementService(DirectContentServiceDependencies dependencies)
|
||||
: DirectContentServiceBase(dependencies), IOperationContentManagementService
|
||||
{
|
||||
public async Task<CatalogList<OperationContentItem>> GetOperationContentAsync(
|
||||
DirectContentActor actor,
|
||||
@@ -72,4 +73,4 @@ public sealed partial class DirectContentService
|
||||
|
||||
return new ContentManagementResult<OperationContentItem>(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@ using Tiku.Domain.QuestionBanks;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
internal sealed class QuestionManagementService(DirectContentServiceDependencies dependencies)
|
||||
: DirectContentServiceBase(dependencies), IQuestionManagementService
|
||||
{
|
||||
public async Task<ContentManagementResult<QuestionManagementItem>> CreateQuestionAsync(
|
||||
DirectContentActor actor,
|
||||
@@ -112,4 +113,4 @@ public sealed partial class DirectContentService
|
||||
if (transaction is not null) await transaction.CommitAsync(cancellationToken);
|
||||
return new ContentManagementResult<QuestionManagementItem>(ToQuestionItem(question, version));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,8 @@ using Tiku.Infrastructure.Security;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
internal sealed class ScorelineManagementService(DirectContentServiceDependencies dependencies)
|
||||
: DirectContentServiceBase(dependencies), IScorelineManagementService
|
||||
{
|
||||
public async Task<CatalogList<ScorelineField>> GetScorelineFieldsAsync(
|
||||
DirectContentActor actor,
|
||||
@@ -185,4 +186,4 @@ public sealed partial class DirectContentService
|
||||
|
||||
return new CatalogList<ScorelineTrendItem>(items);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@ using Tiku.Domain.QuestionBanks;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
internal sealed class VideoManagementService(DirectContentServiceDependencies dependencies)
|
||||
: DirectContentServiceBase(dependencies), IVideoManagementService
|
||||
{
|
||||
public async Task<CatalogList<VideoManagementItem>> GetVideosAsync(
|
||||
DirectContentActor actor,
|
||||
@@ -96,4 +97,4 @@ public sealed partial class DirectContentService
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new ContentManagementResult<QuestionVideoManagementItem>(ToQuestionVideoItem(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,8 @@ using Tiku.Infrastructure.Security;
|
||||
|
||||
namespace Tiku.Infrastructure.Content;
|
||||
|
||||
public sealed partial class DirectContentService
|
||||
internal sealed class VocabularyManagementService(DirectContentServiceDependencies dependencies)
|
||||
: DirectContentServiceBase(dependencies), IVocabularyManagementService
|
||||
{
|
||||
public async Task<CatalogList<VocabularyUnit>> GetVocabularyUnitsAsync(
|
||||
DirectContentActor actor,
|
||||
@@ -147,4 +148,4 @@ public sealed partial class DirectContentService
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
return new ContentManagementResult<VocabularyWord>(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,15 @@ internal static class ContentModule
|
||||
services.AddScoped<ITaxonomyService, TaxonomyService>();
|
||||
services.AddScoped<IContentNavigationQueryService, ContentNavigationQueryService>();
|
||||
services.AddScoped<IContentManagementService, ContentManagementService>();
|
||||
services.AddScoped<IDirectContentService, DirectContentService>();
|
||||
services.AddScoped<DirectContentServiceDependencies>();
|
||||
services.AddScoped<IQuestionManagementService, QuestionManagementService>();
|
||||
services.AddScoped<IVocabularyManagementService, VocabularyManagementService>();
|
||||
services.AddScoped<IHandbookManagementService, HandbookManagementService>();
|
||||
services.AddScoped<IEducationCatalogManagementService, EducationCatalogManagementService>();
|
||||
services.AddScoped<IScorelineManagementService, ScorelineManagementService>();
|
||||
services.AddScoped<IVideoManagementService, VideoManagementService>();
|
||||
services.AddScoped<IOperationContentManagementService, OperationContentManagementService>();
|
||||
services.AddScoped<IContentImportService, ContentImportService>();
|
||||
services.AddScoped<IQuestionBankQueryService, QuestionBankQueryService>();
|
||||
services.AddScoped<IPublicQuestionAccessPolicy, PublicQuestionAccessPolicy>();
|
||||
services.AddScoped<IQuestionReferenceService, QuestionReferenceService>();
|
||||
|
||||
@@ -35,7 +35,6 @@ public sealed class ArchitectureBoundaryTests
|
||||
var root = FindRepositoryRoot();
|
||||
var legacyLineBudgets = new Dictionary<string, int>(StringComparer.Ordinal)
|
||||
{
|
||||
["DirectContentService"] = 2015,
|
||||
["CommerceAdminService"] = 1842,
|
||||
["LearningActivityService"] = 1781,
|
||||
["PlatformAdminService"] = 1719,
|
||||
|
||||
Reference in New Issue
Block a user