using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OutputCaching; using Tiku.Api.Contracts; using Tiku.Api.Security; using Tiku.Application.Assets; using Tiku.Application.Catalog; using Tiku.Application.Content; using Tiku.Application.QuestionBanks; using Tiku.Application.Security; using Tiku.Application.StudyContent; using Tiku.Application.Tenancy; namespace Tiku.Api.Controllers; [ApiController] [Tags("学生端-公开目录")] [AllowAnonymous] [Produces("application/json")] [Route("api/public/catalog")] [OutputCache(PolicyName = "TenantPublic")] public sealed class CatalogController( ICatalogQueryService catalogQueryService, IContentNavigationQueryService contentNavigationQueryService, IQuestionBankQueryService questionBankQueryService, IStudyContentQueryService studyContentQueryService, IAssetQueryService assetQueryService, ITenantContext currentTenant, ITenantDirectory tenantDirectory) : 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")] [RequireSaasFeature(SaasFeatureCatalog.Practice)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Practice)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Practice)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Practice)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Practice)] [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}")] [RequireSaasFeature(SaasFeatureCatalog.Practice)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Practice)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Vocabulary)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Vocabulary)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Handbook)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Handbook)] [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")] [RequireSaasFeature(SaasFeatureCatalog.Handbook)] [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)); } [HttpGet("content-assets")] [EndpointSummary("查询内容资源")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetContentAssets( [FromQuery] AssetQueryDto query, CancellationToken cancellationToken) { return Ok(await assetQueryService.GetContentAssetsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("images")] [EndpointSummary("查询图片资源")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetImages( [FromQuery] AssetQueryDto query, CancellationToken cancellationToken) { return Ok(await assetQueryService.GetImagesAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("app-assets")] [EndpointSummary("查询应用资源")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetAppAssets( [FromQuery] AssetQueryDto query, CancellationToken cancellationToken) { return Ok(await assetQueryService.GetAppAssetsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("video-explanations")] [RequireSaasFeature(SaasFeatureCatalog.Video)] [EndpointSummary("查询视频讲解")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetVideoExplanations( [FromQuery] AssetQueryDto query, CancellationToken cancellationToken) { return Ok(await assetQueryService.GetVideoExplanationsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("question-videos")] [RequireSaasFeature(SaasFeatureCatalog.Video)] [EndpointSummary("查询题目关联视频")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetQuestionVideos( [FromQuery] AssetQueryDto query, CancellationToken cancellationToken) { return Ok(await assetQueryService.GetQuestionVideosAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("banners")] [RequireSaasFeature(SaasFeatureCatalog.SiteContent)] [EndpointSummary("查询首页横幅")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetBanners( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetBannersAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("faqs")] [RequireSaasFeature(SaasFeatureCatalog.SiteContent)] [EndpointSummary("查询常见问题")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetFaqs( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetFaqsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("announcements")] [RequireSaasFeature(SaasFeatureCatalog.SiteContent)] [EndpointSummary("查询公告")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetAnnouncements( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetAnnouncementsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("exam-dates")] [RequireSaasFeature(SaasFeatureCatalog.SiteContent)] [EndpointSummary("查询考试日期")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetExamDates( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetExamDatesAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("products")] [RequireSaasFeature(SaasFeatureCatalog.StudentStore)] [EndpointSummary("查询可购买产品")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetProducts( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetProductsAsync( query.ToFilter(await ResolveTenantIdAsync(query, cancellationToken)), cancellationToken)); } [HttpGet("svip-plans")] [RequireSaasFeature(SaasFeatureCatalog.StudentStore)] [EndpointSummary("查询 SVIP 套餐")] [ProducesResponseType>(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task>> GetSvipPlans( [FromQuery] CatalogQueryDto query, CancellationToken cancellationToken) { return Ok(await catalogQueryService.GetSvipPlansAsync( 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 tenant = await tenantDirectory.FindByCodeAsync(tenantCode, cancellationToken); return tenant?.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); } private Task ResolveTenantIdAsync( AssetQueryDto query, CancellationToken cancellationToken) { return ResolveTenantIdAsync( new CatalogQueryDto { TenantCode = query.TenantCode }, cancellationToken); } } public sealed class TenantNotFoundException : Exception { public TenantNotFoundException() : base("Tenant was not found.") { } }