From 750346fdb2ea31fac814e75652c7a9355d5745ba Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Fri, 20 Feb 2026 16:23:29 +0800
Subject: [PATCH] feat(api): add tenant files upload endpoint
---
.../Requests/FileUploadFormRequest.cs | 27 +++++++
.../Controllers/FilesController.cs | 79 +++++++++++++++++++
2 files changed, 106 insertions(+)
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Contracts/Requests/FileUploadFormRequest.cs
create mode 100644 src/Api/TakeoutSaaS.TenantApi/Controllers/FilesController.cs
diff --git a/src/Api/TakeoutSaaS.TenantApi/Contracts/Requests/FileUploadFormRequest.cs b/src/Api/TakeoutSaaS.TenantApi/Contracts/Requests/FileUploadFormRequest.cs
new file mode 100644
index 0000000..83018c8
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Contracts/Requests/FileUploadFormRequest.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Http;
+
+namespace TakeoutSaaS.TenantApi.Contracts.Requests;
+
+///
+/// 文件上传表单请求。
+///
+public sealed record FileUploadFormRequest
+{
+ ///
+ /// 上传文件。
+ ///
+ [Required]
+ public required IFormFile File { get; init; }
+
+ ///
+ /// 租户 ID。
+ ///
+ [Required]
+ public long? TenantId { get; init; }
+
+ ///
+ /// 上传类型。
+ ///
+ public string? Type { get; init; }
+}
diff --git a/src/Api/TakeoutSaaS.TenantApi/Controllers/FilesController.cs b/src/Api/TakeoutSaaS.TenantApi/Controllers/FilesController.cs
new file mode 100644
index 0000000..e4ebeba
--- /dev/null
+++ b/src/Api/TakeoutSaaS.TenantApi/Controllers/FilesController.cs
@@ -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;
+
+///
+/// 租户端文件上传。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/tenant/v{version:apiVersion}/files")]
+public sealed class FilesController(
+ IFileStorageService fileStorageService,
+ ITenantProvider tenantProvider) : BaseApiController
+{
+ ///
+ /// 上传图片或文件。
+ ///
+ /// 文件上传响应信息。
+ [HttpPost("upload")]
+ [Consumes("multipart/form-data")]
+ [RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status403Forbidden)]
+ public async Task> Upload([FromForm] FileUploadFormRequest request, CancellationToken cancellationToken)
+ {
+ // 1. 校验文件有效性
+ if (request.File is null || request.File.Length == 0)
+ {
+ return ApiResponse.Error(ErrorCodes.BadRequest, "文件不能为空");
+ }
+
+ // 2. 校验租户标识参数
+ if (!request.TenantId.HasValue || request.TenantId.Value <= 0)
+ {
+ return ApiResponse.Error(ErrorCodes.BadRequest, "TenantId 不能为空");
+ }
+
+ // 3. 校验当前租户上下文
+ var currentTenantId = tenantProvider.GetCurrentTenantId();
+ if (currentTenantId <= 0)
+ {
+ return ApiResponse.Error(ErrorCodes.BadRequest, "缺少租户标识");
+ }
+
+ if (request.TenantId.Value != currentTenantId)
+ {
+ return ApiResponse.Error(ErrorCodes.Forbidden, "禁止跨租户上传文件");
+ }
+
+ // 4. 解析上传类型
+ if (!UploadFileTypeParser.TryParse(request.Type, out var uploadType))
+ {
+ return ApiResponse.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.Ok(result);
+ }
+}