diff --git a/src/Api/Directory.Build.props b/src/Api/Directory.Build.props
new file mode 100644
index 0000000..474a084
--- /dev/null
+++ b/src/Api/Directory.Build.props
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/Api/TakeoutSaaS.AdminApi/Contracts/Requests/FileUploadFormRequest.cs b/src/Api/TakeoutSaaS.AdminApi/Contracts/Requests/FileUploadFormRequest.cs
new file mode 100644
index 0000000..056faa3
--- /dev/null
+++ b/src/Api/TakeoutSaaS.AdminApi/Contracts/Requests/FileUploadFormRequest.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Http;
+
+namespace TakeoutSaaS.AdminApi.Contracts.Requests;
+
+///
+/// 文件上传表单请求。
+///
+public sealed record FileUploadFormRequest
+{
+ ///
+ /// 上传文件。
+ ///
+ [Required]
+ public required IFormFile File { get; init; }
+ ///
+ /// 上传类型。
+ ///
+ public string? Type { get; init; }
+}
diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs
index cd92566..35f57f5 100644
--- a/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs
+++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/FilesController.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.AdminApi.Contracts.Requests;
using TakeoutSaaS.Application.Storage.Abstractions;
using TakeoutSaaS.Application.Storage.Contracts;
using TakeoutSaaS.Application.Storage.Extensions;
@@ -22,32 +23,29 @@ public sealed class FilesController(IFileStorageService fileStorageService) : Ba
///
/// 文件上传响应信息。
[HttpPost("upload")]
+ [Consumes("multipart/form-data")]
[RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)]
- public async Task> Upload([FromForm] IFormFile? file, [FromForm] string? type, CancellationToken cancellationToken)
+ public async Task> Upload([FromForm] FileUploadFormRequest request, CancellationToken cancellationToken)
{
// 1. 校验文件有效性
- if (file == null || file.Length == 0)
+ if (request.File is null || request.File.Length == 0)
{
return ApiResponse.Error(ErrorCodes.BadRequest, "文件不能为空");
}
-
// 2. 解析上传类型
- if (!UploadFileTypeParser.TryParse(type, out var uploadType))
+ if (!UploadFileTypeParser.TryParse(request.Type, out var uploadType))
{
return ApiResponse.Error(ErrorCodes.BadRequest, "上传类型不合法");
}
-
// 3. 提取请求来源
var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault();
- await using var stream = file.OpenReadStream();
-
+ await using var stream = request.File.OpenReadStream();
// 4. 调用存储服务执行上传
var result = await fileStorageService.UploadAsync(
- new UploadFileRequest(uploadType, stream, file.FileName, file.ContentType ?? string.Empty, file.Length, origin),
+ new UploadFileRequest(uploadType, stream, request.File.FileName, request.File.ContentType ?? string.Empty, request.File.Length, origin),
cancellationToken);
-
// 5. 返回上传结果
return ApiResponse.Ok(result);
}
diff --git a/src/Api/TakeoutSaaS.MiniApi/Contracts/Requests/FileUploadFormRequest.cs b/src/Api/TakeoutSaaS.MiniApi/Contracts/Requests/FileUploadFormRequest.cs
new file mode 100644
index 0000000..8cc3e95
--- /dev/null
+++ b/src/Api/TakeoutSaaS.MiniApi/Contracts/Requests/FileUploadFormRequest.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+using Microsoft.AspNetCore.Http;
+
+namespace TakeoutSaaS.MiniApi.Contracts.Requests;
+
+///
+/// 文件上传表单请求。
+///
+public sealed record FileUploadFormRequest
+{
+ ///
+ /// 上传文件。
+ ///
+ [Required]
+ public required IFormFile File { get; init; }
+ ///
+ /// 上传类型。
+ ///
+ public string? Type { get; init; }
+}
diff --git a/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs b/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs
index 0d651fb..c06c440 100644
--- a/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs
+++ b/src/Api/TakeoutSaaS.MiniApi/Controllers/FilesController.cs
@@ -6,6 +6,7 @@ using TakeoutSaaS.Application.Storage.Extensions;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api;
+using TakeoutSaaS.MiniApi.Contracts.Requests;
namespace TakeoutSaaS.MiniApi.Controllers;
@@ -20,37 +21,33 @@ public sealed class FilesController(IFileStorageService fileStorageService) : Ba
///
/// 上传图片或文件。
///
- /// 上传文件。
- /// 上传类型。
+ /// 表单请求,包含文件与类型。
/// 取消标记。
/// 上传结果,包含访问链接等信息。
[HttpPost("upload")]
+ [Consumes("multipart/form-data")]
[RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status400BadRequest)]
- public async Task> Upload([FromForm] IFormFile? file, [FromForm] string? type, CancellationToken cancellationToken)
+ public async Task> Upload([FromForm] FileUploadFormRequest request, CancellationToken cancellationToken)
{
// 1. 校验文件有效性
- if (file == null || file.Length == 0)
+ if (request.File is null || request.File.Length == 0)
{
return ApiResponse.Error(ErrorCodes.BadRequest, "文件不能为空");
}
-
// 2. 解析上传类型
- if (!UploadFileTypeParser.TryParse(type, out var uploadType))
+ if (!UploadFileTypeParser.TryParse(request.Type, out var uploadType))
{
return ApiResponse.Error(ErrorCodes.BadRequest, "上传类型不合法");
}
-
// 3. 提取请求来源
var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault();
- await using var stream = file.OpenReadStream();
-
+ await using var stream = request.File.OpenReadStream();
// 4. 调用存储服务执行上传
var result = await fileStorageService.UploadAsync(
- new UploadFileRequest(uploadType, stream, file.FileName, file.ContentType ?? string.Empty, file.Length, origin),
+ new UploadFileRequest(uploadType, stream, request.File.FileName, request.File.ContentType ?? string.Empty, request.File.Length, origin),
cancellationToken);
-
// 5. 返回上传结果
return ApiResponse.Ok(result);
}
diff --git a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs
index 7ac65ce..842728d 100644
--- a/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Core/TakeoutSaaS.Shared.Web/Extensions/ServiceCollectionExtensions.cs
@@ -1,9 +1,10 @@
+using Asp.Versioning;
+using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Web.Filters;
using TakeoutSaaS.Shared.Web.Security;
-
namespace TakeoutSaaS.Shared.Web.Extensions;
///
@@ -16,10 +17,11 @@ public static class ServiceCollectionExtensions
///
public static IServiceCollection AddSharedWebCore(this IServiceCollection services)
{
+ // 1. 注册基础上下文与当前用户访问器
services.AddHttpContextAccessor();
services.AddEndpointsApiExplorer();
services.AddScoped();
-
+ // 2. 注册控制器与全局过滤器
services
.AddControllers(options =>
{
@@ -27,25 +29,25 @@ public static class ServiceCollectionExtensions
options.Filters.Add();
})
.AddNewtonsoftJson();
-
+ // 3. 配置模型验证行为
services.Configure(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
-
- services.AddApiVersioning(options =>
+ // 4. 配置 API 版本化
+ var apiVersioningBuilder = services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
});
-
- services.AddVersionedApiExplorer(setup =>
+ // 5. 注册版本化 Api Explorer
+ apiVersioningBuilder.AddApiExplorer(setup =>
{
setup.GroupNameFormat = "'v'VVV";
setup.SubstituteApiVersionInUrl = true;
});
-
+ // 6. 返回服务集合
return services;
}
}
diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs
index 68590fc..cfa1753 100644
--- a/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs
+++ b/src/Core/TakeoutSaaS.Shared.Web/Swagger/ConfigureSwaggerOptions.cs
@@ -1,9 +1,8 @@
-using Microsoft.AspNetCore.Mvc.ApiExplorer;
+using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
-
namespace TakeoutSaaS.Shared.Web.Swagger;
///
@@ -15,8 +14,12 @@ internal sealed class ConfigureSwaggerOptions(
{
private readonly SwaggerDocumentSettings _settings = settings.Value;
+ ///
+ /// 根据 API 版本生成 Swagger 文档配置。
+ ///
public void Configure(SwaggerGenOptions options)
{
+ // 1. 为每个 API 版本注册文档
foreach (var description in provider.ApiVersionDescriptions)
{
var info = new OpenApiInfo
@@ -27,14 +30,13 @@ internal sealed class ConfigureSwaggerOptions(
? $"{_settings.Description}(该版本已弃用)"
: _settings.Description
};
-
options.SwaggerGeneratorOptions.SwaggerDocs[description.GroupName] = info;
}
+ // 2. 配置 JWT 授权信息
if (_settings.EnableAuthorization)
{
const string bearerSchemeName = "Bearer";
-
var scheme = new OpenApiSecurityScheme
{
Name = "Authorization",
@@ -44,7 +46,6 @@ internal sealed class ConfigureSwaggerOptions(
Scheme = "bearer",
BearerFormat = "JWT"
};
-
options.AddSecurityDefinition(bearerSchemeName, scheme);
options.AddSecurityRequirement(document =>
{
@@ -52,7 +53,6 @@ internal sealed class ConfigureSwaggerOptions(
{
{ new OpenApiSecuritySchemeReference(bearerSchemeName, document, null), new List() }
};
-
return requirement;
});
}
diff --git a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs
index 51385fb..29a839c 100644
--- a/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs
+++ b/src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs
@@ -1,5 +1,7 @@
+using System;
+using System.IO;
+using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
@@ -17,7 +19,16 @@ public static class SwaggerExtensions
///
public static IServiceCollection AddSharedSwagger(this IServiceCollection services, Action? configure = null)
{
- services.AddSwaggerGen();
+ // 1. 注册 Swagger 并加载 XML 注释以展示中文文档
+ services.AddSwaggerGen(options =>
+ {
+ var basePath = AppContext.BaseDirectory;
+ var xmlFiles = Directory.GetFiles(basePath, "*.xml");
+ foreach (var xml in xmlFiles)
+ {
+ options.IncludeXmlComments(xml, true);
+ }
+ });
services.AddSingleton(_ =>
{
var settings = new SwaggerDocumentSettings();
@@ -28,7 +39,6 @@ public static class SwaggerExtensions
new ConfigureSwaggerOptions(
provider.GetRequiredService(),
Options.Create(provider.GetRequiredService())));
-
return services;
}
@@ -39,7 +49,7 @@ public static class SwaggerExtensions
{
var provider = app.ApplicationServices.GetRequiredService();
var settings = app.ApplicationServices.GetRequiredService();
-
+ // 1. 注册 Swagger 中间件
app.UseSwagger();
app.UseSwaggerUI(options =>
{
@@ -49,10 +59,9 @@ public static class SwaggerExtensions
$"/swagger/{description.GroupName}/swagger.json",
$"{settings.Title} {description.ApiVersion}");
}
-
+ // 2. 显示请求耗时
options.DisplayRequestDuration();
});
-
return app;
}
}
diff --git a/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj b/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj
index b387355..53aaa82 100644
--- a/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj
+++ b/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj
@@ -9,13 +9,12 @@
-
-
+
+
-