feat(api): add tenant files upload endpoint
All checks were successful
Build and Deploy TenantApi / build-and-deploy (push) Successful in 49s

This commit is contained in:
2026-02-20 16:23:29 +08:00
parent 48cf852d46
commit 750346fdb2
2 changed files with 106 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
namespace TakeoutSaaS.TenantApi.Contracts.Requests;
/// <summary>
/// 文件上传表单请求。
/// </summary>
public sealed record FileUploadFormRequest
{
/// <summary>
/// 上传文件。
/// </summary>
[Required]
public required IFormFile File { get; init; }
/// <summary>
/// 租户 ID。
/// </summary>
[Required]
public long? TenantId { get; init; }
/// <summary>
/// 上传类型。
/// </summary>
public string? Type { get; init; }
}

View File

@@ -0,0 +1,79 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TakeoutSaaS.Application.Storage.Abstractions;
using TakeoutSaaS.Application.Storage.Contracts;
using TakeoutSaaS.Application.Storage.Extensions;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
using TakeoutSaaS.Shared.Web.Api;
using TakeoutSaaS.TenantApi.Contracts.Requests;
namespace TakeoutSaaS.TenantApi.Controllers;
/// <summary>
/// 租户端文件上传。
/// </summary>
[ApiVersion("1.0")]
[Authorize]
[Route("api/tenant/v{version:apiVersion}/files")]
public sealed class FilesController(
IFileStorageService fileStorageService,
ITenantProvider tenantProvider) : BaseApiController
{
/// <summary>
/// 上传图片或文件。
/// </summary>
/// <returns>文件上传响应信息。</returns>
[HttpPost("upload")]
[Consumes("multipart/form-data")]
[RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status403Forbidden)]
public async Task<ApiResponse<FileUploadResponse>> Upload([FromForm] FileUploadFormRequest request, CancellationToken cancellationToken)
{
// 1. 校验文件有效性
if (request.File is null || request.File.Length == 0)
{
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "文件不能为空");
}
// 2. 校验租户标识参数
if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
{
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "TenantId 不能为空");
}
// 3. 校验当前租户上下文
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId <= 0)
{
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "缺少租户标识");
}
if (request.TenantId.Value != currentTenantId)
{
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.Forbidden, "禁止跨租户上传文件");
}
// 4. 解析上传类型
if (!UploadFileTypeParser.TryParse(request.Type, out var uploadType))
{
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "上传类型不合法");
}
// 5. 提取请求来源
var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault();
await using var stream = request.File.OpenReadStream();
// 6. 调用存储服务执行上传
var result = await fileStorageService.UploadAsync(
new UploadFileRequest(uploadType, stream, request.File.FileName, request.File.ContentType ?? string.Empty, request.File.Length, origin),
cancellationToken);
// 7. 返回上传结果
return ApiResponse<FileUploadResponse>.Ok(result);
}
}