forked from gongxuegit/tiku-backend.net
feat: add tenant content navigation management endpoints
This commit is contained in:
363
Tiku.Api/Contracts/ContentManagementDtos.cs
Normal file
363
Tiku.Api/Contracts/ContentManagementDtos.cs
Normal file
@@ -0,0 +1,363 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Domain.Common;
|
||||
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
public sealed class ContentManagementQueryDto
|
||||
{
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
public Guid? NodeId { get; set; }
|
||||
|
||||
public Guid? CollectionId { get; set; }
|
||||
|
||||
[StringLength(64)]
|
||||
[RegularExpression("^(root|[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})$")]
|
||||
public string? ParentId { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? EntryType { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? CollectionType { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? Mode { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? MarkerType { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? Keyword { get; set; }
|
||||
|
||||
public bool IncludeInactive { get; set; }
|
||||
|
||||
[Range(1, 1000)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public ContentManagementFilter ToFilter()
|
||||
{
|
||||
return new ContentManagementFilter(
|
||||
RegionId,
|
||||
EntryId,
|
||||
NodeId,
|
||||
CollectionId,
|
||||
ParentId,
|
||||
EntryType,
|
||||
CollectionType,
|
||||
Mode,
|
||||
MarkerType,
|
||||
Keyword,
|
||||
IncludeInactive,
|
||||
Limit);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpsertContentEntryDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
[StringLength(64)]
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? EntryKey { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string? EntryType { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? Icon { get; set; }
|
||||
|
||||
[StringLength(500)]
|
||||
public string? Route { get; set; }
|
||||
|
||||
[StringLength(2000)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? Visibility { get; set; }
|
||||
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public JsonElement LayoutConfig { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public int? Order { get; set; }
|
||||
|
||||
public bool? IsActive { get; set; }
|
||||
|
||||
public UpsertContentEntryCommand ToCommand()
|
||||
{
|
||||
return new UpsertContentEntryCommand(
|
||||
Id,
|
||||
RegionId,
|
||||
LegacyId,
|
||||
EntryKey,
|
||||
Name,
|
||||
EntryType,
|
||||
Icon,
|
||||
Route,
|
||||
Description,
|
||||
Visibility,
|
||||
AccessRules,
|
||||
LayoutConfig,
|
||||
Order,
|
||||
IsActive);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpsertContentNodeDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public Guid EntryId { get; set; }
|
||||
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
[StringLength(64)]
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? NodeKey { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string? NodeType { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? MarkerType { get; set; }
|
||||
|
||||
public JsonElement MarkerConfig { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public int? Order { get; set; }
|
||||
|
||||
public bool? IsActive { get; set; }
|
||||
|
||||
public bool? IsSelectable { get; set; }
|
||||
|
||||
public bool? IsLeaf { get; set; }
|
||||
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertContentNodeCommand ToCommand()
|
||||
{
|
||||
return new UpsertContentNodeCommand(
|
||||
Id,
|
||||
EntryId,
|
||||
RegionId,
|
||||
ParentId,
|
||||
LegacyId,
|
||||
NodeKey,
|
||||
Name,
|
||||
NodeType,
|
||||
MarkerType,
|
||||
MarkerConfig,
|
||||
Order,
|
||||
IsActive,
|
||||
IsSelectable,
|
||||
IsLeaf,
|
||||
AccessRules,
|
||||
Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpsertQuestionCollectionDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
public Guid? NodeId { get; set; }
|
||||
|
||||
public Guid? SubjectId { get; set; }
|
||||
|
||||
public Guid? CategoryId { get; set; }
|
||||
|
||||
public Guid? QuestionBankId { get; set; }
|
||||
|
||||
[StringLength(64)]
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string? CollectionType { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? SourceType { get; set; }
|
||||
|
||||
public JsonElement Filters { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public decimal? TotalScore { get; set; }
|
||||
|
||||
public int? DurationMinutes { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
public int? Order { get; set; }
|
||||
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public UpsertQuestionCollectionCommand ToCommand()
|
||||
{
|
||||
return new UpsertQuestionCollectionCommand(
|
||||
Id,
|
||||
RegionId,
|
||||
EntryId,
|
||||
NodeId,
|
||||
SubjectId,
|
||||
CategoryId,
|
||||
QuestionBankId,
|
||||
LegacyId,
|
||||
Name,
|
||||
CollectionType,
|
||||
SourceType,
|
||||
Filters,
|
||||
TotalScore,
|
||||
DurationMinutes,
|
||||
Status,
|
||||
Order,
|
||||
AccessRules,
|
||||
Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CollectionQuestionDto
|
||||
{
|
||||
[Required]
|
||||
public Guid QuestionId { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string? SectionKey { get; set; }
|
||||
|
||||
public int? Order { get; set; }
|
||||
|
||||
public decimal? Score { get; set; }
|
||||
|
||||
public bool? Required { get; set; }
|
||||
|
||||
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public CollectionQuestionCommand ToCommand()
|
||||
{
|
||||
return new CollectionQuestionCommand(QuestionId, SectionKey, Order, Score, Required, Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ReplaceCollectionItemsDto
|
||||
{
|
||||
[Required]
|
||||
public Guid CollectionId { get; set; }
|
||||
|
||||
public IReadOnlyCollection<CollectionQuestionDto> Questions { get; set; } = [];
|
||||
|
||||
public ReplaceCollectionItemsCommand ToCommand()
|
||||
{
|
||||
return new ReplaceCollectionItemsCommand(
|
||||
CollectionId,
|
||||
Questions.Select(question => question.ToCommand()).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class UpsertPracticeBlueprintDto
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
public Guid? RegionId { get; set; }
|
||||
|
||||
public Guid? EntryId { get; set; }
|
||||
|
||||
public Guid? NodeId { get; set; }
|
||||
|
||||
public Guid? CollectionId { get; set; }
|
||||
|
||||
[StringLength(64)]
|
||||
public string? LegacyId { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string? Mode { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string? AssemblyType { get; set; }
|
||||
|
||||
public int? QuestionLimit { get; set; }
|
||||
|
||||
public int? DurationMinutes { get; set; }
|
||||
|
||||
public decimal? TotalScore { get; set; }
|
||||
|
||||
public decimal? PassScore { get; set; }
|
||||
|
||||
public JsonElement Sections { get; set; } = JsonDefaults.Array();
|
||||
|
||||
public JsonElement Rules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
public JsonElement AccessRules { get; set; } = JsonDefaults.Object();
|
||||
|
||||
[StringLength(50)]
|
||||
public string? Status { get; set; }
|
||||
|
||||
public int? Order { get; set; }
|
||||
|
||||
public UpsertPracticeBlueprintCommand ToCommand()
|
||||
{
|
||||
return new UpsertPracticeBlueprintCommand(
|
||||
Id,
|
||||
RegionId,
|
||||
EntryId,
|
||||
NodeId,
|
||||
CollectionId,
|
||||
LegacyId,
|
||||
Name,
|
||||
Mode,
|
||||
AssemblyType,
|
||||
QuestionLimit,
|
||||
DurationMinutes,
|
||||
TotalScore,
|
||||
PassScore,
|
||||
Sections,
|
||||
Rules,
|
||||
AccessRules,
|
||||
Status,
|
||||
Order);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ImportTemplateQueryDto
|
||||
{
|
||||
[Required]
|
||||
[StringLength(50)]
|
||||
public string ImportType { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(10)]
|
||||
public string? Format { get; set; }
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Security;
|
||||
|
||||
namespace Tiku.Api.Controllers;
|
||||
@@ -13,9 +14,145 @@ namespace Tiku.Api.Controllers;
|
||||
[Route("api/tenant-content")]
|
||||
public sealed class TenantContentController(
|
||||
IAssetManagementService assetManagementService,
|
||||
IContentManagementService contentManagementService,
|
||||
ICurrentUser currentUser,
|
||||
ICurrentTenant currentTenant) : ControllerBase
|
||||
{
|
||||
[HttpGet("entries")]
|
||||
[EndpointSummary("查询租户内容入口")]
|
||||
[ProducesResponseType<CatalogList<ContentEntryManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ContentEntryManagementItem>>> GetEntries(
|
||||
[FromQuery] ContentManagementQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentManagementService.GetEntriesAsync(
|
||||
ResolveContentActor(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("entries")]
|
||||
[EndpointSummary("创建或更新内容入口")]
|
||||
[ProducesResponseType<ContentManagementResult<ContentEntryManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<ContentEntryManagementItem>>> UpsertEntry(
|
||||
UpsertContentEntryDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentManagementService.UpsertEntryAsync(
|
||||
ResolveContentActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("nodes")]
|
||||
[EndpointSummary("查询租户内容节点")]
|
||||
[ProducesResponseType<CatalogList<ContentNodeManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<ContentNodeManagementItem>>> GetNodes(
|
||||
[FromQuery] ContentManagementQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentManagementService.GetNodesAsync(
|
||||
ResolveContentActor(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("nodes")]
|
||||
[EndpointSummary("创建或更新内容节点")]
|
||||
[ProducesResponseType<ContentManagementResult<ContentNodeManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<ContentNodeManagementItem>>> UpsertNode(
|
||||
UpsertContentNodeDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentManagementService.UpsertNodeAsync(
|
||||
ResolveContentActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("question-collections")]
|
||||
[EndpointSummary("查询租户题集")]
|
||||
[ProducesResponseType<CatalogList<QuestionCollectionManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<QuestionCollectionManagementItem>>> GetQuestionCollections(
|
||||
[FromQuery] ContentManagementQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentManagementService.GetCollectionsAsync(
|
||||
ResolveContentActor(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("question-collections")]
|
||||
[EndpointSummary("创建或更新题集")]
|
||||
[ProducesResponseType<ContentManagementResult<QuestionCollectionManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<QuestionCollectionManagementItem>>> UpsertQuestionCollection(
|
||||
UpsertQuestionCollectionDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentManagementService.UpsertCollectionAsync(
|
||||
ResolveContentActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("question-collections/items/replace")]
|
||||
[EndpointSummary("替换题集题目")]
|
||||
[ProducesResponseType<CollectionItemsReplaceResult>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CollectionItemsReplaceResult>> ReplaceQuestionCollectionItems(
|
||||
ReplaceCollectionItemsDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentManagementService.ReplaceCollectionItemsAsync(
|
||||
ResolveContentActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("practice-blueprints")]
|
||||
[EndpointSummary("查询练习蓝图")]
|
||||
[ProducesResponseType<CatalogList<PracticeBlueprintManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<CatalogList<PracticeBlueprintManagementItem>>> GetPracticeBlueprints(
|
||||
[FromQuery] ContentManagementQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentManagementService.GetPracticeBlueprintsAsync(
|
||||
ResolveContentActor(),
|
||||
query.ToFilter(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpPost("practice-blueprints")]
|
||||
[EndpointSummary("创建或更新练习蓝图")]
|
||||
[ProducesResponseType<ContentManagementResult<PracticeBlueprintManagementItem>>(StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<ContentManagementResult<PracticeBlueprintManagementItem>>> UpsertPracticeBlueprint(
|
||||
UpsertPracticeBlueprintDto request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentManagementService.UpsertPracticeBlueprintAsync(
|
||||
ResolveContentActor(),
|
||||
request.ToCommand(),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("imports/field-mapping")]
|
||||
[EndpointSummary("查询导入字段映射")]
|
||||
[ProducesResponseType<ImportFieldMappingItem>(StatusCodes.Status200OK)]
|
||||
public ActionResult<ImportFieldMappingItem> GetImportFieldMapping([FromQuery] ImportTemplateQueryDto query)
|
||||
{
|
||||
_ = ResolveContentActor();
|
||||
return Ok(contentManagementService.GetImportFieldMapping(query.ImportType));
|
||||
}
|
||||
|
||||
[HttpGet("imports/templates")]
|
||||
[EndpointSummary("获取内容导入模板")]
|
||||
[ProducesResponseType<ImportTemplateItem>(StatusCodes.Status200OK)]
|
||||
public ActionResult<ImportTemplateItem> GetImportTemplate([FromQuery] ImportTemplateQueryDto query)
|
||||
{
|
||||
_ = ResolveContentActor();
|
||||
return Ok(contentManagementService.GetImportTemplate(query.ImportType, query.Format));
|
||||
}
|
||||
|
||||
[HttpGet("assets")]
|
||||
[EndpointSummary("查询租户内容资产")]
|
||||
[ProducesResponseType<CatalogList<ContentAssetManagementItem>>(StatusCodes.Status200OK)]
|
||||
@@ -95,4 +232,14 @@ public sealed class TenantContentController(
|
||||
|
||||
return new AssetManagementActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
|
||||
private ContentManagementActor ResolveContentActor()
|
||||
{
|
||||
if (currentTenant.TenantId is null || currentUser.UserId is null)
|
||||
{
|
||||
throw new ContentManagementException("Tenant content actor was not resolved.", "tenant_content_access_denied");
|
||||
}
|
||||
|
||||
return new ContentManagementActor(currentTenant.TenantId.Value, currentUser.UserId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Controllers;
|
||||
using Tiku.Application.Assets;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Storage;
|
||||
using Tiku.Infrastructure.Content;
|
||||
using Tiku.Infrastructure.Learning;
|
||||
@@ -98,6 +99,16 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is ContentManagementException contentManagementException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
contentManagementException.Message,
|
||||
ContentManagementStatusCode(contentManagementException.Code),
|
||||
contentManagementException.Code);
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is LearningValidationException learningValidationException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
@@ -230,4 +241,15 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
}
|
||||
|
||||
private static int ContentManagementStatusCode(string code)
|
||||
{
|
||||
return code switch
|
||||
{
|
||||
"entry_not_found" or "node_not_found" or "collection_not_found" or "question_not_found" or
|
||||
"import_type_invalid" => StatusCodes.Status404NotFound,
|
||||
"tenant_content_access_denied" => StatusCodes.Status403Forbidden,
|
||||
_ => StatusCodes.Status400BadRequest
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user