forked from gongxuegit/tiku-backend.net
feat: add tenant content asset management endpoints
This commit is contained in:
177
Tiku.Api/Contracts/AssetManagementDtos.cs
Normal file
177
Tiku.Api/Contracts/AssetManagementDtos.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
98
Tiku.Api/Controllers/TenantContentController.cs
Normal file
98
Tiku.Api/Controllers/TenantContentController.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user