forked from gongxuegit/tiku-backend.net
feat: add content navigation readonly endpoints
This commit is contained in:
71
Tiku.Api/Contracts/ContentNavigationDtos.cs
Normal file
71
Tiku.Api/Contracts/ContentNavigationDtos.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Tiku.Application.Content;
|
||||
|
||||
namespace Tiku.Api.Contracts;
|
||||
|
||||
public sealed class ContentNavigationQueryDto
|
||||
{
|
||||
[StringLength(100)]
|
||||
public string? TenantCode { get; set; }
|
||||
|
||||
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 IncludeHidden { get; set; }
|
||||
|
||||
public bool IncludeInactive { get; set; }
|
||||
|
||||
[Range(1, 1000)]
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public ContentNavigationFilter ToFilter(Guid tenantId)
|
||||
{
|
||||
var parentWasSpecified = ParentId is not null;
|
||||
var parentIsRoot = string.Equals(ParentId, "root", StringComparison.OrdinalIgnoreCase);
|
||||
Guid? parentId = parentIsRoot || string.IsNullOrWhiteSpace(ParentId)
|
||||
? null
|
||||
: Guid.Parse(ParentId);
|
||||
|
||||
return new ContentNavigationFilter(
|
||||
tenantId,
|
||||
RegionId,
|
||||
EntryId,
|
||||
NodeId,
|
||||
CollectionId,
|
||||
parentId,
|
||||
parentWasSpecified,
|
||||
parentIsRoot,
|
||||
EntryType,
|
||||
CollectionType,
|
||||
Mode,
|
||||
MarkerType,
|
||||
Keyword,
|
||||
IncludeHidden,
|
||||
IncludeInactive,
|
||||
Limit);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Api.Contracts;
|
||||
using Tiku.Application.Catalog;
|
||||
using Tiku.Application.Content;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
@@ -15,6 +16,7 @@ namespace Tiku.Api.Controllers;
|
||||
[Route("api/catalog")]
|
||||
public sealed class CatalogController(
|
||||
ICatalogQueryService catalogQueryService,
|
||||
IContentNavigationQueryService contentNavigationQueryService,
|
||||
ICurrentTenant currentTenant,
|
||||
TikuDbContext dbContext) : ControllerBase
|
||||
{
|
||||
@@ -110,6 +112,73 @@ public sealed class CatalogController(
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("content-entries")]
|
||||
[EndpointSummary("查询内容入口")]
|
||||
[ProducesResponseType<CatalogList<ContentEntryCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CatalogList<ContentEntryCatalogItem>>> GetContentEntries(
|
||||
[FromQuery] ContentNavigationQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentNavigationQueryService.GetContentEntriesAsync(
|
||||
query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("content-nodes")]
|
||||
[EndpointSummary("查询内容导航节点")]
|
||||
[ProducesResponseType<CatalogList<ContentNodeCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CatalogList<ContentNodeCatalogItem>>> GetContentNodes(
|
||||
[FromQuery] ContentNavigationQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentNavigationQueryService.GetContentNodesAsync(
|
||||
query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("question-collections")]
|
||||
[EndpointSummary("查询可用题集")]
|
||||
[ProducesResponseType<CatalogList<QuestionCollectionCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CatalogList<QuestionCollectionCatalogItem>>> GetQuestionCollections(
|
||||
[FromQuery] ContentNavigationQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentNavigationQueryService.GetQuestionCollectionsAsync(
|
||||
query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("question-collections/questions")]
|
||||
[EndpointSummary("查询题集内题目")]
|
||||
[ProducesResponseType<CatalogList<CollectionQuestionCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CatalogList<CollectionQuestionCatalogItem>>> GetCollectionQuestions(
|
||||
[FromQuery] ContentNavigationQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentNavigationQueryService.GetCollectionQuestionsAsync(
|
||||
query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
[HttpGet("practice-blueprints")]
|
||||
[EndpointSummary("查询练习蓝图")]
|
||||
[ProducesResponseType<CatalogList<PracticeBlueprintCatalogItem>>(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
|
||||
public async Task<ActionResult<CatalogList<PracticeBlueprintCatalogItem>>> GetPracticeBlueprints(
|
||||
[FromQuery] ContentNavigationQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return Ok(await contentNavigationQueryService.GetPracticeBlueprintsAsync(
|
||||
query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)),
|
||||
cancellationToken));
|
||||
}
|
||||
|
||||
private async Task<Guid> ResolveTenantIdAsync(
|
||||
CatalogQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -134,6 +203,18 @@ public sealed class CatalogController(
|
||||
|
||||
return tenantId ?? throw new TenantNotFoundException();
|
||||
}
|
||||
|
||||
private Task<Guid> ResolveTenantIdAsync(
|
||||
ContentNavigationQueryDto query,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return ResolveTenantIdAsync(
|
||||
new CatalogQueryDto
|
||||
{
|
||||
TenantCode = query.TenantCode
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TenantNotFoundException : Exception
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Tiku.Api.Controllers;
|
||||
using Tiku.Application.Auth;
|
||||
using Tiku.Infrastructure.Content;
|
||||
|
||||
namespace Tiku.Api.Middleware;
|
||||
|
||||
@@ -33,6 +34,26 @@ public sealed class ExceptionHandlingMiddleware(
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is RequiredFieldException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
exception.Message,
|
||||
StatusCodes.Status400BadRequest,
|
||||
"required_field");
|
||||
return;
|
||||
}
|
||||
|
||||
if (exception is ContentNavigationNotFoundException)
|
||||
{
|
||||
await WriteProblemAsync(
|
||||
context,
|
||||
exception.Message,
|
||||
StatusCodes.Status404NotFound,
|
||||
"content_navigation_not_found");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogError(exception, "Unhandled API exception");
|
||||
|
||||
var problem = new ProblemDetails
|
||||
|
||||
Reference in New Issue
Block a user