using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Tiku.Api.Contracts; using Tiku.Application.Catalog; using Tiku.Application.Content; using Tiku.Application.QuestionBanks; using Tiku.Application.Security; using Tiku.Application.StudyContent; using Tiku.Domain.Tenancy; using Tiku.Infrastructure.Persistence; namespace Tiku.Api.Controllers; [ApiController] [AllowAnonymous] [Produces("application/json")] [Route("api/catalog")] public sealed class CatalogController( ICatalogQueryService catalogQueryService, IContentNavigationQueryService contentNavigationQueryService, IQuestionBankQueryService questionBankQueryService, IStudyContentQueryService studyContentQueryService, ICurrentTenant currentTenant, TikuDbContext dbContext) : ControllerBase { [HttpGet("regions")] [EndpointSummary("查询可用地区")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetRegions( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetRegionsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("region-modules")] [EndpointSummary("查询地区功能模块")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetRegionModules( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetRegionModulesAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("module-nodes")] [EndpointSummary("查询模块导航节点")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetModuleNodes( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetModuleNodesAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("schools")] [EndpointSummary("查询院校目录")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetSchools( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetSchoolsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("majors")] [EndpointSummary("查询专业目录")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetMajors( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetMajorsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("subjects")] [EndpointSummary("查询科目目录")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetSubjects( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetSubjectsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("categories")] [HttpGet("question-categories")] [EndpointSummary("查询题目分类")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetCategories( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetCategoriesAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("content-entries")] [EndpointSummary("查询内容入口")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetContentEntries( [FromQuery] ContentNavigationQueryDto query, CancellationToken cancellationToken) { return Ok(await contentNavigationQueryService.GetContentEntriesAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("content-nodes")] [EndpointSummary("查询内容导航节点")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetContentNodes( [FromQuery] ContentNavigationQueryDto query, CancellationToken cancellationToken) { return Ok(await contentNavigationQueryService.GetContentNodesAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("question-collections")] [EndpointSummary("查询可用题集")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetQuestionCollections( [FromQuery] ContentNavigationQueryDto query, CancellationToken cancellationToken) { return Ok(await contentNavigationQueryService.GetQuestionCollectionsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("question-collections/questions")] [EndpointSummary("查询题集内题目")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetCollectionQuestions( [FromQuery] ContentNavigationQueryDto query, CancellationToken cancellationToken) { return Ok(await contentNavigationQueryService.GetCollectionQuestionsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("practice-blueprints")] [EndpointSummary("查询练习蓝图")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetPracticeBlueprints( [FromQuery] ContentNavigationQueryDto query, CancellationToken cancellationToken) { return Ok(await contentNavigationQueryService.GetPracticeBlueprintsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("question-banks")] [EndpointSummary("查询题库列表")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetQuestionBanks( [FromQuery] QuestionBankQueryDto query, CancellationToken cancellationToken) { return Ok(await questionBankQueryService.GetQuestionBanksAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("questions")] [EndpointSummary("查询已发布题目")] [EndpointDescription("支持按题库、科目、分类、模块节点、内容入口、内容节点、题集或题目 ID 列表筛选。")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetQuestions( [FromQuery] QuestionBankQueryDto query, CancellationToken cancellationToken) { return Ok(await questionBankQueryService.GetQuestionsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("questions/{questionId:guid}")] [EndpointSummary("查询题目详情")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetQuestion( Guid questionId, [FromQuery] QuestionBankQueryDto query, CancellationToken cancellationToken) { return Ok(await questionBankQueryService.GetQuestionAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken), questionId), cancellationToken)); } [HttpGet("questions/{questionId:guid}/versions")] [EndpointSummary("查询题目版本")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetQuestionVersions( Guid questionId, [FromQuery] QuestionBankQueryDto query, CancellationToken cancellationToken) { return Ok(await questionBankQueryService.GetQuestionVersionsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken), questionId), cancellationToken)); } [HttpGet("vocabulary-units")] [EndpointSummary("查询词汇单元")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetVocabularyUnits( [FromQuery] StudyContentQueryDto query, CancellationToken cancellationToken) { return Ok(await studyContentQueryService.GetVocabularyUnitsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("vocabulary-words")] [EndpointSummary("查询词汇单词")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetVocabularyWords( [FromQuery] StudyContentQueryDto query, CancellationToken cancellationToken) { return Ok(await studyContentQueryService.GetVocabularyWordsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("handbook-subjects")] [EndpointSummary("查询知识手册科目")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetHandbookSubjects( [FromQuery] StudyContentQueryDto query, CancellationToken cancellationToken) { return Ok(await studyContentQueryService.GetHandbookSubjectsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("handbook-chapters")] [EndpointSummary("查询知识手册章节")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetHandbookChapters( [FromQuery] StudyContentQueryDto query, CancellationToken cancellationToken) { return Ok(await studyContentQueryService.GetHandbookChaptersAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("handbook-entries")] [EndpointSummary("查询知识手册条目")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetHandbookEntries( [FromQuery] StudyContentQueryDto query, CancellationToken cancellationToken) { return Ok(await studyContentQueryService.GetHandbookEntriesAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } private async Task ResolveTenantIdAsync( CatalogQueryDto query, CancellationToken cancellationToken) { if (currentTenant.TenantId.HasValue) { return currentTenant.TenantId.Value; } var tenantCode = query.TenantCode ?? Request.Headers["x-tenant-code"].FirstOrDefault(); if (string.IsNullOrWhiteSpace(tenantCode)) { throw new TenantNotFoundException(); } var tenantId = await dbContext.Tenants .Where(tenant => tenant.Slug == tenantCode.Trim() && tenant.Status == TenantStatus.Active) .Select(tenant => (Guid?)tenant.Id) .SingleOrDefaultAsync(cancellationToken); return tenantId ?? throw new TenantNotFoundException(); } private Task ResolveTenantIdAsync( ContentNavigationQueryDto query, CancellationToken cancellationToken) { return ResolveTenantIdAsync( new CatalogQueryDto { TenantCode = query.TenantCode }, cancellationToken); } private Task ResolveTenantIdAsync( QuestionBankQueryDto query, CancellationToken cancellationToken) { return ResolveTenantIdAsync( new CatalogQueryDto { TenantCode = query.TenantCode }, cancellationToken); } private Task ResolveTenantIdAsync( StudyContentQueryDto query, CancellationToken cancellationToken) { return ResolveTenantIdAsync( new CatalogQueryDto { TenantCode = query.TenantCode }, cancellationToken); } } public sealed class TenantNotFoundException : Exception { public TenantNotFoundException() : base("Tenant was not found.") { } }