using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Tiku.Api.Contracts; 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, IBackgroundJobService backgroundJobService, ICurrentUser currentUser, ITenantContext currentTenant) : ControllerBase { [HttpPost("questions")] [Authorize(Policy = BackendPermissions.TenantContentManage)] [Authorize(Policy = TikuPolicies.TenantAllDataScope)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)] [EndpointSummary("创建题目及首个版本")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> CreateQuestion( DirectQuestionWriteDto request, CancellationToken cancellationToken) { return Ok(await directContentService.CreateQuestionAsync(ResolveActor(), request.ToCommand(createVersionDefault: true), cancellationToken)); } [HttpPatch("questions")] [Authorize(Policy = BackendPermissions.TenantContentManage)] [Authorize(Policy = TikuPolicies.TenantAllDataScope)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.PrivateQuestionBank)] [EndpointSummary("更新题目并可选择创建新版本")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpdateQuestion( DirectQuestionWriteDto request, CancellationToken cancellationToken) { return Ok(await directContentService.UpdateQuestionAsync(ResolveActor(), request.ToCommand(createVersionDefault: false), cancellationToken)); } [HttpGet("vocabulary-units")] [Authorize(Policy = BackendPermissions.TenantVocabularyManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)] [EndpointSummary("查询管理侧词汇单元")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetVocabularyUnits( [FromQuery] DirectContentQueryDto query, CancellationToken cancellationToken) { return Ok(await directContentService.GetVocabularyUnitsAsync(ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPut("vocabulary-units")] [Authorize(Policy = BackendPermissions.TenantVocabularyManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)] [EndpointSummary("新增或更新词汇单元")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)] [EndpointSummary("查询管理侧词汇")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Vocabulary)] [EndpointSummary("新增或更新词汇")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertVocabularyWord( DirectVocabularyWordDto request, CancellationToken cancellationToken) { return Ok(await directContentService.UpsertVocabularyWordAsync(ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("handbook-subjects")] [Authorize(Policy = BackendPermissions.TenantHandbookManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)] [EndpointSummary("查询管理侧知识手册科目")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetHandbookSubjects( [FromQuery] DirectContentQueryDto query, CancellationToken cancellationToken) { return Ok(await directContentService.GetHandbookSubjectsAsync(ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPut("handbook-subjects")] [Authorize(Policy = BackendPermissions.TenantHandbookManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)] [EndpointSummary("新增或更新知识手册科目")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)] [EndpointSummary("查询管理侧知识手册章节")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)] [EndpointSummary("新增或更新知识手册章节")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)] [EndpointSummary("查询管理侧知识手册条目")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Handbook)] [EndpointSummary("新增或更新知识手册条目")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertHandbookEntry( DirectHandbookEntryDto request, CancellationToken cancellationToken) { return Ok(await directContentService.UpsertHandbookEntryAsync(ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("scoreline/schools")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("查询管理侧分数线院校")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetSchools( [FromQuery] DirectContentQueryDto query, CancellationToken cancellationToken) { return Ok(await directContentService.GetSchoolsAsync(ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPut("scoreline/schools")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("新增或更新分数线院校")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertSchool( DirectSchoolDto request, CancellationToken cancellationToken) { return Ok(await directContentService.UpsertSchoolAsync(ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("scoreline/majors")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("查询管理侧分数线专业")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetMajors( [FromQuery] DirectContentQueryDto query, CancellationToken cancellationToken) { return Ok(await directContentService.GetMajorsAsync(ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPut("scoreline/majors")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("新增或更新分数线专业")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertMajor( DirectMajorDto request, CancellationToken cancellationToken) { return Ok(await directContentService.UpsertMajorAsync(ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("scoreline/fields")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("查询管理侧分数线字段")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetScorelineFields( [FromQuery] DirectContentQueryDto query, CancellationToken cancellationToken) { return Ok(await directContentService.GetScorelineFieldsAsync(ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPut("scoreline/fields")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("新增或更新动态分数线字段")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertScorelineField( DirectScorelineFieldDto request, CancellationToken cancellationToken) { return Ok(await directContentService.UpsertScorelineFieldAsync(ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("scoreline/records")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("查询管理侧分数线记录")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetScorelineRecords( [FromQuery] DirectContentQueryDto query, CancellationToken cancellationToken) { return Ok(await directContentService.GetScorelineRecordsAsync(ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPut("scoreline/records")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("新增或更新分数线记录")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertScorelineRecord( DirectScorelineRecordDto request, CancellationToken cancellationToken) { return Ok(await directContentService.UpsertScorelineRecordAsync(ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("scoreline/years")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("查询分数线年份")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetScorelineYears( [FromQuery] DirectContentQueryDto query, CancellationToken cancellationToken) { return Ok(await directContentService.GetScorelineYearsAsync(ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("scoreline/trend")] [Authorize(Policy = BackendPermissions.TenantScorelineManage)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Scoreline)] [EndpointSummary("查询分数线趋势摘要")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)] [EndpointSummary("查询租户视频解析")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)] [EndpointSummary("新增或更新视频解析")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.Video)] [EndpointSummary("绑定题目与解析视频")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)] [EndpointSummary("查询运营内容")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeature(SaasFeatureCatalog.SiteContent)] [EndpointSummary("新增或更新运营内容")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> 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)] [Tiku.Api.Security.RequireSaasFeatureFromRoute("importType")] [EndpointSummary("预览内容导入数据")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> PreviewImport( string importType, DirectImportDto request, CancellationToken cancellationToken) { return Ok(await directContentService.PreviewImportAsync(ResolveActor(), request.ToCommand(importType, dryRun: true), cancellationToken)); } [HttpPost("imports/{importType}")] [Authorize(Policy = BackendPermissions.TenantJobManage)] [Authorize(Policy = TikuPolicies.TenantAllDataScope)] [Tiku.Api.Security.RequireSaasFeatureFromRoute("importType")] [EndpointSummary("执行或排队内容导入")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status202Accepted)] public async Task> ExecuteImport( string importType, DirectImportDto request, CancellationToken cancellationToken) { var actor = ResolveActor(); var command = request.ToCommand(importType, dryRun: false); if (request.Async == true || command.Items.Count > 100) { var job = await backgroundJobService.EnqueueAsync( new CreateBackgroundJobCommand( actor.TenantId, "content_import", System.Text.Json.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(StatusCodes.Status200OK)] public async Task> 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>(StatusCodes.Status200OK)] public async Task>> 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(StatusCodes.Status200OK)] public async Task> 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(StatusCodes.Status200OK)] public async Task> 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); } }