feat: add tenant content asset management endpoints

This commit is contained in:
xiong
2026-07-26 15:34:59 +08:00
parent e505384ecf
commit 3c0acebbbe
8 changed files with 1402 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Tiku.Application.Assets;
using Tiku.Domain.Common;
namespace Tiku.Api.Contracts;
public sealed class AssetUploadSignDto
{
public Guid? AssetId { get; set; }
public Guid? RegionId { get; set; }
public Guid? SubjectId { get; set; }
public Guid? CategoryId { get; set; }
public Guid? ContentNodeId { get; set; }
[StringLength(200)]
public string? AssetKey { get; set; }
[StringLength(300)]
public string? Title { get; set; }
[StringLength(100)]
public string? Category { get; set; }
[StringLength(1000)]
public string? Description { get; set; }
[Required]
[StringLength(500)]
public string FileName { get; set; } = string.Empty;
[Required]
[StringLength(200)]
public string MimeType { get; set; } = string.Empty;
[Range(0, long.MaxValue)]
public long? FileSizeBytes { get; set; }
[RegularExpression("^[A-Fa-f0-9]{64}$")]
public string? ChecksumSha256 { get; set; }
[StringLength(50)]
public string? AssetType { get; set; }
[StringLength(50)]
public string? Visibility { get; set; }
public bool? IsPublic { get; set; }
[StringLength(50)]
public string? Provider { get; set; }
[StringLength(200)]
public string? Bucket { get; set; }
[StringLength(1000)]
public string? ObjectKey { get; set; }
[Range(1, 3600)]
public int? ExpiresInSeconds { get; set; }
public JsonElement Metadata { get; set; } = JsonDefaults.Object();
public AssetUploadSignCommand ToCommand()
{
return new AssetUploadSignCommand(
AssetId,
RegionId,
SubjectId,
CategoryId,
ContentNodeId,
AssetKey,
Title,
Category,
Description,
FileName,
MimeType,
FileSizeBytes,
ChecksumSha256,
AssetType,
Visibility,
IsPublic,
Provider,
Bucket,
ObjectKey,
ExpiresInSeconds,
Metadata);
}
}
public sealed class AssetUploadConfirmDto
{
[Required]
public Guid AssetId { get; set; }
[StringLength(200)]
public string? MimeType { get; set; }
[Range(0, long.MaxValue)]
public long? FileSizeBytes { get; set; }
[RegularExpression("^[A-Fa-f0-9]{64}$")]
public string? ChecksumSha256 { get; set; }
public AssetUploadConfirmCommand ToCommand()
{
return new AssetUploadConfirmCommand(AssetId, MimeType, FileSizeBytes, ChecksumSha256);
}
}
public sealed class AssetManagementQueryDto
{
public Guid? RegionId { get; set; }
public Guid? SubjectId { get; set; }
public Guid? CategoryId { get; set; }
public Guid? ContentNodeId { get; set; }
[StringLength(50)]
public string? AssetType { get; set; }
[StringLength(100)]
public string? Category { get; set; }
[StringLength(50)]
public string? UploadStatus { get; set; }
[StringLength(50)]
public string? SecurityScanStatus { get; set; }
[StringLength(100)]
public string? Keyword { get; set; }
[Range(1, 500)]
public int? Limit { get; set; }
public AssetManagementFilter ToFilter()
{
return new AssetManagementFilter(
RegionId,
SubjectId,
CategoryId,
ContentNodeId,
AssetType,
Category,
UploadStatus,
SecurityScanStatus,
Keyword,
Limit);
}
}
public sealed class ImportJobQueryDto
{
[StringLength(50)]
public string? Status { get; set; }
[StringLength(50)]
public string? ImportType { get; set; }
[StringLength(50)]
public string? SourceFormat { get; set; }
[Range(1, 500)]
public int? Limit { get; set; }
public ImportJobFilter ToFilter()
{
return new ImportJobFilter(Status, ImportType, SourceFormat, Limit);
}
}

View File

@@ -0,0 +1,98 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Tiku.Api.Contracts;
using Tiku.Application.Assets;
using Tiku.Application.Catalog;
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,
ICurrentUser currentUser,
ICurrentTenant currentTenant) : ControllerBase
{
[HttpGet("assets")]
[EndpointSummary("查询租户内容资产")]
[ProducesResponseType<CatalogList<ContentAssetManagementItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ContentAssetManagementItem>>> GetAssets(
[FromQuery] AssetManagementQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.GetAssetsAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpPost("assets/uploads/sign")]
[EndpointSummary("创建资产上传签名")]
[ProducesResponseType<AssetUploadSignResult>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status401Unauthorized)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status403Forbidden)]
public async Task<ActionResult<AssetUploadSignResult>> SignAssetUpload(
AssetUploadSignDto request,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.SignUploadAsync(
ResolveActor(),
request.ToCommand(),
cancellationToken));
}
[HttpPost("assets/uploads/confirm")]
[EndpointSummary("确认资产上传完成")]
[ProducesResponseType<AssetUploadConfirmResult>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status409Conflict)]
public async Task<ActionResult<AssetUploadConfirmResult>> ConfirmAssetUpload(
AssetUploadConfirmDto request,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.ConfirmUploadAsync(
ResolveActor(),
request.ToCommand(),
cancellationToken));
}
[HttpGet("import-jobs")]
[EndpointSummary("查询内容导入任务")]
[ProducesResponseType<CatalogList<ContentImportJobItem>>(StatusCodes.Status200OK)]
public async Task<ActionResult<CatalogList<ContentImportJobItem>>> GetImportJobs(
[FromQuery] ImportJobQueryDto query,
CancellationToken cancellationToken)
{
return Ok(await assetManagementService.GetImportJobsAsync(
ResolveActor(),
query.ToFilter(),
cancellationToken));
}
[HttpGet("import-jobs/{jobId:guid}")]
[EndpointSummary("查询内容导入任务详情")]
[ProducesResponseType<ContentImportJobDetail>(StatusCodes.Status200OK)]
[ProducesResponseType<ProblemDetails>(StatusCodes.Status404NotFound)]
public async Task<ActionResult<ContentImportJobDetail>> 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);
}
}

View File

@@ -88,6 +88,16 @@ public sealed class ExceptionHandlingMiddleware(
return;
}
if (exception is AssetManagementException assetManagementException)
{
await WriteProblemAsync(
context,
assetManagementException.Message,
AssetManagementStatusCode(assetManagementException.Code),
assetManagementException.Code);
return;
}
if (exception is LearningValidationException learningValidationException)
{
await WriteProblemAsync(
@@ -209,4 +219,15 @@ public sealed class ExceptionHandlingMiddleware(
_ => StatusCodes.Status400BadRequest
};
}
private static int AssetManagementStatusCode(string code)
{
return code switch
{
"asset_not_found" or "import_job_not_found" => StatusCodes.Status404NotFound,
"asset_upload_missing" => StatusCodes.Status409Conflict,
"tenant_content_access_denied" => StatusCodes.Status403Forbidden,
_ => StatusCodes.Status400BadRequest
};
}
}