fix: 修复文件上传Swagger生成并加载中文注释

This commit is contained in:
2025-12-04 20:18:26 +08:00
parent 1d7836a173
commit ba9be6bbc4
9 changed files with 93 additions and 43 deletions

View File

@@ -0,0 +1,5 @@
<Project>
<ItemGroup>
<Using Include="Asp.Versioning" />
</ItemGroup>
</Project>

View File

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

View File

@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using TakeoutSaaS.AdminApi.Contracts.Requests;
using TakeoutSaaS.Application.Storage.Abstractions; using TakeoutSaaS.Application.Storage.Abstractions;
using TakeoutSaaS.Application.Storage.Contracts; using TakeoutSaaS.Application.Storage.Contracts;
using TakeoutSaaS.Application.Storage.Extensions; using TakeoutSaaS.Application.Storage.Extensions;
@@ -22,32 +23,29 @@ public sealed class FilesController(IFileStorageService fileStorageService) : Ba
/// </summary> /// </summary>
/// <returns>文件上传响应信息。</returns> /// <returns>文件上传响应信息。</returns>
[HttpPost("upload")] [HttpPost("upload")]
[Consumes("multipart/form-data")]
[RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)] [RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status400BadRequest)]
public async Task<ApiResponse<FileUploadResponse>> Upload([FromForm] IFormFile? file, [FromForm] string? type, CancellationToken cancellationToken) public async Task<ApiResponse<FileUploadResponse>> Upload([FromForm] FileUploadFormRequest request, CancellationToken cancellationToken)
{ {
// 1. 校验文件有效性 // 1. 校验文件有效性
if (file == null || file.Length == 0) if (request.File is null || request.File.Length == 0)
{ {
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "文件不能为空"); return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "文件不能为空");
} }
// 2. 解析上传类型 // 2. 解析上传类型
if (!UploadFileTypeParser.TryParse(type, out var uploadType)) if (!UploadFileTypeParser.TryParse(request.Type, out var uploadType))
{ {
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "上传类型不合法"); return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "上传类型不合法");
} }
// 3. 提取请求来源 // 3. 提取请求来源
var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault(); var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault();
await using var stream = file.OpenReadStream(); await using var stream = request.File.OpenReadStream();
// 4. 调用存储服务执行上传 // 4. 调用存储服务执行上传
var result = await fileStorageService.UploadAsync( 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); cancellationToken);
// 5. 返回上传结果 // 5. 返回上传结果
return ApiResponse<FileUploadResponse>.Ok(result); return ApiResponse<FileUploadResponse>.Ok(result);
} }

View File

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

View File

@@ -6,6 +6,7 @@ using TakeoutSaaS.Application.Storage.Extensions;
using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Web.Api; using TakeoutSaaS.Shared.Web.Api;
using TakeoutSaaS.MiniApi.Contracts.Requests;
namespace TakeoutSaaS.MiniApi.Controllers; namespace TakeoutSaaS.MiniApi.Controllers;
@@ -20,37 +21,33 @@ public sealed class FilesController(IFileStorageService fileStorageService) : Ba
/// <summary> /// <summary>
/// 上传图片或文件。 /// 上传图片或文件。
/// </summary> /// </summary>
/// <param name="file">上传文件。</param> /// <param name="request">表单请求,包含文件与类型。</param>
/// <param name="type">上传类型。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <returns>上传结果,包含访问链接等信息。</returns> /// <returns>上传结果,包含访问链接等信息。</returns>
[HttpPost("upload")] [HttpPost("upload")]
[Consumes("multipart/form-data")]
[RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)] [RequestFormLimits(MultipartBodyLengthLimit = 30 * 1024 * 1024)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ApiResponse<FileUploadResponse>), StatusCodes.Status400BadRequest)]
public async Task<ApiResponse<FileUploadResponse>> Upload([FromForm] IFormFile? file, [FromForm] string? type, CancellationToken cancellationToken) public async Task<ApiResponse<FileUploadResponse>> Upload([FromForm] FileUploadFormRequest request, CancellationToken cancellationToken)
{ {
// 1. 校验文件有效性 // 1. 校验文件有效性
if (file == null || file.Length == 0) if (request.File is null || request.File.Length == 0)
{ {
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "文件不能为空"); return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "文件不能为空");
} }
// 2. 解析上传类型 // 2. 解析上传类型
if (!UploadFileTypeParser.TryParse(type, out var uploadType)) if (!UploadFileTypeParser.TryParse(request.Type, out var uploadType))
{ {
return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "上传类型不合法"); return ApiResponse<FileUploadResponse>.Error(ErrorCodes.BadRequest, "上传类型不合法");
} }
// 3. 提取请求来源 // 3. 提取请求来源
var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault(); var origin = Request.Headers["Origin"].FirstOrDefault() ?? Request.Headers["Referer"].FirstOrDefault();
await using var stream = file.OpenReadStream(); await using var stream = request.File.OpenReadStream();
// 4. 调用存储服务执行上传 // 4. 调用存储服务执行上传
var result = await fileStorageService.UploadAsync( 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); cancellationToken);
// 5. 返回上传结果 // 5. 返回上传结果
return ApiResponse<FileUploadResponse>.Ok(result); return ApiResponse<FileUploadResponse>.Ok(result);
} }

View File

@@ -1,9 +1,10 @@
using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Web.Filters; using TakeoutSaaS.Shared.Web.Filters;
using TakeoutSaaS.Shared.Web.Security; using TakeoutSaaS.Shared.Web.Security;
namespace TakeoutSaaS.Shared.Web.Extensions; namespace TakeoutSaaS.Shared.Web.Extensions;
/// <summary> /// <summary>
@@ -16,10 +17,11 @@ public static class ServiceCollectionExtensions
/// </summary> /// </summary>
public static IServiceCollection AddSharedWebCore(this IServiceCollection services) public static IServiceCollection AddSharedWebCore(this IServiceCollection services)
{ {
// 1. 注册基础上下文与当前用户访问器
services.AddHttpContextAccessor(); services.AddHttpContextAccessor();
services.AddEndpointsApiExplorer(); services.AddEndpointsApiExplorer();
services.AddScoped<ICurrentUserAccessor, HttpContextCurrentUserAccessor>(); services.AddScoped<ICurrentUserAccessor, HttpContextCurrentUserAccessor>();
// 2. 注册控制器与全局过滤器
services services
.AddControllers(options => .AddControllers(options =>
{ {
@@ -27,25 +29,25 @@ public static class ServiceCollectionExtensions
options.Filters.Add<ApiResponseResultFilter>(); options.Filters.Add<ApiResponseResultFilter>();
}) })
.AddNewtonsoftJson(); .AddNewtonsoftJson();
// 3. 配置模型验证行为
services.Configure<ApiBehaviorOptions>(options => services.Configure<ApiBehaviorOptions>(options =>
{ {
options.SuppressModelStateInvalidFilter = true; options.SuppressModelStateInvalidFilter = true;
}); });
// 4. 配置 API 版本化
services.AddApiVersioning(options => var apiVersioningBuilder = services.AddApiVersioning(options =>
{ {
options.AssumeDefaultVersionWhenUnspecified = true; options.AssumeDefaultVersionWhenUnspecified = true;
options.DefaultApiVersion = new ApiVersion(1, 0); options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true; options.ReportApiVersions = true;
}); });
// 5. 注册版本化 Api Explorer
services.AddVersionedApiExplorer(setup => apiVersioningBuilder.AddApiExplorer(setup =>
{ {
setup.GroupNameFormat = "'v'VVV"; setup.GroupNameFormat = "'v'VVV";
setup.SubstituteApiVersionInUrl = true; setup.SubstituteApiVersionInUrl = true;
}); });
// 6. 返回服务集合
return services; return services;
} }
} }

View File

@@ -1,9 +1,8 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer; using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.OpenApi; using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
namespace TakeoutSaaS.Shared.Web.Swagger; namespace TakeoutSaaS.Shared.Web.Swagger;
/// <summary> /// <summary>
@@ -15,8 +14,12 @@ internal sealed class ConfigureSwaggerOptions(
{ {
private readonly SwaggerDocumentSettings _settings = settings.Value; private readonly SwaggerDocumentSettings _settings = settings.Value;
/// <summary>
/// 根据 API 版本生成 Swagger 文档配置。
/// </summary>
public void Configure(SwaggerGenOptions options) public void Configure(SwaggerGenOptions options)
{ {
// 1. 为每个 API 版本注册文档
foreach (var description in provider.ApiVersionDescriptions) foreach (var description in provider.ApiVersionDescriptions)
{ {
var info = new OpenApiInfo var info = new OpenApiInfo
@@ -27,14 +30,13 @@ internal sealed class ConfigureSwaggerOptions(
? $"{_settings.Description}(该版本已弃用)" ? $"{_settings.Description}(该版本已弃用)"
: _settings.Description : _settings.Description
}; };
options.SwaggerGeneratorOptions.SwaggerDocs[description.GroupName] = info; options.SwaggerGeneratorOptions.SwaggerDocs[description.GroupName] = info;
} }
// 2. 配置 JWT 授权信息
if (_settings.EnableAuthorization) if (_settings.EnableAuthorization)
{ {
const string bearerSchemeName = "Bearer"; const string bearerSchemeName = "Bearer";
var scheme = new OpenApiSecurityScheme var scheme = new OpenApiSecurityScheme
{ {
Name = "Authorization", Name = "Authorization",
@@ -44,7 +46,6 @@ internal sealed class ConfigureSwaggerOptions(
Scheme = "bearer", Scheme = "bearer",
BearerFormat = "JWT" BearerFormat = "JWT"
}; };
options.AddSecurityDefinition(bearerSchemeName, scheme); options.AddSecurityDefinition(bearerSchemeName, scheme);
options.AddSecurityRequirement(document => options.AddSecurityRequirement(document =>
{ {
@@ -52,7 +53,6 @@ internal sealed class ConfigureSwaggerOptions(
{ {
{ new OpenApiSecuritySchemeReference(bearerSchemeName, document, null), new List<string>() } { new OpenApiSecuritySchemeReference(bearerSchemeName, document, null), new List<string>() }
}; };
return requirement; return requirement;
}); });
} }

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using Asp.Versioning.ApiExplorer;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -17,7 +19,16 @@ public static class SwaggerExtensions
/// </summary> /// </summary>
public static IServiceCollection AddSharedSwagger(this IServiceCollection services, Action<SwaggerDocumentSettings>? configure = null) public static IServiceCollection AddSharedSwagger(this IServiceCollection services, Action<SwaggerDocumentSettings>? 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(_ => services.AddSingleton(_ =>
{ {
var settings = new SwaggerDocumentSettings(); var settings = new SwaggerDocumentSettings();
@@ -28,7 +39,6 @@ public static class SwaggerExtensions
new ConfigureSwaggerOptions( new ConfigureSwaggerOptions(
provider.GetRequiredService<IApiVersionDescriptionProvider>(), provider.GetRequiredService<IApiVersionDescriptionProvider>(),
Options.Create(provider.GetRequiredService<SwaggerDocumentSettings>()))); Options.Create(provider.GetRequiredService<SwaggerDocumentSettings>())));
return services; return services;
} }
@@ -39,7 +49,7 @@ public static class SwaggerExtensions
{ {
var provider = app.ApplicationServices.GetRequiredService<IApiVersionDescriptionProvider>(); var provider = app.ApplicationServices.GetRequiredService<IApiVersionDescriptionProvider>();
var settings = app.ApplicationServices.GetRequiredService<SwaggerDocumentSettings>(); var settings = app.ApplicationServices.GetRequiredService<SwaggerDocumentSettings>();
// 1. 注册 Swagger 中间件
app.UseSwagger(); app.UseSwagger();
app.UseSwaggerUI(options => app.UseSwaggerUI(options =>
{ {
@@ -49,10 +59,9 @@ public static class SwaggerExtensions
$"/swagger/{description.GroupName}/swagger.json", $"/swagger/{description.GroupName}/swagger.json",
$"{settings.Title} {description.ApiVersion}"); $"{settings.Title} {description.ApiVersion}");
} }
// 2. 显示请求耗时
options.DisplayRequestDuration(); options.DisplayRequestDuration();
}); });
return app; return app;
} }
} }

View File

@@ -9,13 +9,12 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" /> <PackageReference Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.1.0" /> <PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="10.0.1" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.1" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.1" />
<PackageReference Include="Microsoft.OpenApi" Version="3.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>