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.Security; namespace Tiku.Api.Controllers; [ApiController] [Authorize(Policy = TikuPolicies.TenantAdmin)] [Produces("application/json")] [Route("api/tenant-content")] public sealed class TenantContentController( IAssetManagementService assetManagementService, IContentManagementService contentManagementService, ICurrentUser currentUser, ITenantContext currentTenant) : ControllerBase { [HttpGet("entries")] [EndpointSummary("查询租户内容入口")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetEntries( [FromQuery] ContentManagementQueryDto query, CancellationToken cancellationToken) { return Ok(await contentManagementService.GetEntriesAsync( ResolveContentActor(), query.ToFilter(), cancellationToken)); } [HttpPost("entries")] [EndpointSummary("创建或更新内容入口")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertEntry( UpsertContentEntryDto request, CancellationToken cancellationToken) { return Ok(await contentManagementService.UpsertEntryAsync( ResolveContentActor(), request.ToCommand(), cancellationToken)); } [HttpGet("nodes")] [EndpointSummary("查询租户内容节点")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetNodes( [FromQuery] ContentManagementQueryDto query, CancellationToken cancellationToken) { return Ok(await contentManagementService.GetNodesAsync( ResolveContentActor(), query.ToFilter(), cancellationToken)); } [HttpPost("nodes")] [EndpointSummary("创建或更新内容节点")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertNode( UpsertContentNodeDto request, CancellationToken cancellationToken) { return Ok(await contentManagementService.UpsertNodeAsync( ResolveContentActor(), request.ToCommand(), cancellationToken)); } [HttpGet("question-collections")] [EndpointSummary("查询租户题集")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetQuestionCollections( [FromQuery] ContentManagementQueryDto query, CancellationToken cancellationToken) { return Ok(await contentManagementService.GetCollectionsAsync( ResolveContentActor(), query.ToFilter(), cancellationToken)); } [HttpPost("question-collections")] [EndpointSummary("创建或更新题集")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertQuestionCollection( UpsertQuestionCollectionDto request, CancellationToken cancellationToken) { return Ok(await contentManagementService.UpsertCollectionAsync( ResolveContentActor(), request.ToCommand(), cancellationToken)); } [HttpPost("question-collections/items/replace")] [EndpointSummary("替换题集题目")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> ReplaceQuestionCollectionItems( ReplaceCollectionItemsDto request, CancellationToken cancellationToken) { return Ok(await contentManagementService.ReplaceCollectionItemsAsync( ResolveContentActor(), request.ToCommand(), cancellationToken)); } [HttpGet("practice-blueprints")] [EndpointSummary("查询练习蓝图")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetPracticeBlueprints( [FromQuery] ContentManagementQueryDto query, CancellationToken cancellationToken) { return Ok(await contentManagementService.GetPracticeBlueprintsAsync( ResolveContentActor(), query.ToFilter(), cancellationToken)); } [HttpPost("practice-blueprints")] [EndpointSummary("创建或更新练习蓝图")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertPracticeBlueprint( UpsertPracticeBlueprintDto request, CancellationToken cancellationToken) { return Ok(await contentManagementService.UpsertPracticeBlueprintAsync( ResolveContentActor(), request.ToCommand(), cancellationToken)); } [HttpGet("imports/field-mapping")] [EndpointSummary("查询导入字段映射")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetImportFieldMapping([FromQuery] ImportTemplateQueryDto query) { _ = ResolveContentActor(); return Ok(contentManagementService.GetImportFieldMapping(query.ImportType)); } [HttpGet("imports/templates")] [EndpointSummary("获取内容导入模板")] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetImportTemplate([FromQuery] ImportTemplateQueryDto query) { _ = ResolveContentActor(); return Ok(contentManagementService.GetImportTemplate(query.ImportType, query.Format)); } [HttpGet("assets")] [EndpointSummary("查询租户内容资产")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetAssets( [FromQuery] AssetManagementQueryDto query, CancellationToken cancellationToken) { return Ok(await assetManagementService.GetAssetsAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("assets/access-events")] [EndpointSummary("查询资产访问审计事件")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetAssetAccessEvents( [FromQuery] AssetEventQueryDto query, CancellationToken cancellationToken) { return Ok(await assetManagementService.GetAccessEventsAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("assets/security-scan-events")] [EndpointSummary("查询资产安全扫描事件")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetAssetSecurityScanEvents( [FromQuery] AssetEventQueryDto query, CancellationToken cancellationToken) { return Ok(await assetManagementService.GetSecurityScanEventsAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpPut("assets")] [EndpointSummary("新增或更新内容资产")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> UpsertAsset( UpsertAssetDto request, CancellationToken cancellationToken) { return Ok(await assetManagementService.UpsertAssetAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpPost("assets/uploads/sign")] [EndpointSummary("创建资产上传签名")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> SignAssetUpload( AssetUploadSignDto request, CancellationToken cancellationToken) { return Ok(await assetManagementService.SignUploadAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpPost("assets/uploads/confirm")] [EndpointSummary("确认资产上传完成")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task> ConfirmAssetUpload( AssetUploadConfirmDto request, CancellationToken cancellationToken) { return Ok(await assetManagementService.ConfirmUploadAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpPost("assets/sign-download")] [EndpointSummary("签发管理侧资产下载地址")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> SignAssetDownload( AssetAccessSignDto request, CancellationToken cancellationToken) { return Ok(await assetManagementService.SignDownloadAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpPost("assets/sign-preview")] [EndpointSummary("签发管理侧资产预览地址")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> SignAssetPreview( AssetAccessSignDto request, CancellationToken cancellationToken) { return Ok(await assetManagementService.SignPreviewAsync( ResolveActor(), request.ToCommand(), cancellationToken)); } [HttpGet("import-jobs")] [EndpointSummary("查询内容导入任务")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task>> GetImportJobs( [FromQuery] ImportJobQueryDto query, CancellationToken cancellationToken) { return Ok(await assetManagementService.GetImportJobsAsync( ResolveActor(), query.ToFilter(), cancellationToken)); } [HttpGet("import-jobs/{jobId:guid}")] [EndpointSummary("查询内容导入任务详情")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetImportJob( Guid jobId, CancellationToken cancellationToken) { return Ok(await assetManagementService.GetImportJobAsync( ResolveActor(), jobId, cancellationToken)); } private AssetManagementActor ResolveActor() { if (currentTenant.TenantId is null || currentUser.UserId is null) { throw new AssetManagementException("Tenant content actor was not resolved.", "tenant_content_access_denied"); } 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); } }