fix swagger, add simapihttpclient

This commit is contained in:
2025-11-02 22:14:34 +08:00
parent c7373da691
commit 65451d7b80
10 changed files with 243 additions and 14 deletions
+52
View File
@@ -0,0 +1,52 @@
using System.Collections.Generic;
using System.Linq;
using SimApi.Attributes;
namespace SimApi.SwaggerFilters;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
/// <summary>
/// 自定义 Swagger 过滤器:将标注 [AesBody] 的参数显示在 Request Body 中
/// </summary>
public class AesBodyOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
foreach (var parameter in context.ApiDescription.ParameterDescriptions)
{
var hasAesBodyAttr = parameter.ParameterInfo()
.GetCustomAttribute<AesBodyAttribute>() != null;
if (!hasAesBodyAttr) continue;
// 1. 移除默认的 Query 参数描述(如果存在)
var queryParam = operation.Parameters
.FirstOrDefault(p => p.Name == parameter.Name);
if (queryParam != null)
{
operation.Parameters.Remove(queryParam);
}
// 2. 添加 Body 参数描述
// 获取参数类型的 SchemaSwagger 模型定义)
var schema = context.SchemaGenerator.GenerateSchema(
parameter.Type,
context.SchemaRepository);
// 将参数添加到 Request Body
operation.RequestBody = new OpenApiRequestBody
{
Content = new Dictionary<string, OpenApiMediaType>
{
{
"application/json", // 假设使用 JSON 格式
new OpenApiMediaType { Schema = schema }
}
},
Description = "内容为加密前内容,需要转换为JSON后使用AES加密后提交,提交格式为 {\"data\":\"AES密文\"}",
Required = true // 标记为必填
};
}
}
}
@@ -0,0 +1,45 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using SimApi.Attributes;
namespace SimApi.SwaggerFilters
{
public class SimApiAuthOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// 检查接口或控制器是否标记了 [SimApiAuth] 特性
var requiresAuth =
// 方法上有 [SimApiAuth]
context.MethodInfo.GetCustomAttributes<SimApiAuthAttribute>(true).Any()
||
// 控制器上有 [SimApiAuth](继承到所有方法)
context.MethodInfo.DeclaringType?.GetCustomAttributes<SimApiAuthAttribute>(true).Any() == true;
if (requiresAuth)
{
// 添加授权要求:关联步骤 2 中定义的 "SimApiAuth" 安全方案
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "SimApiAuth" // 必须与 AddSecurityDefinition 的第一个参数一致
}
},
[] // 无需指定作用域(scope)时留空
}
}
};
}
// 未标记 [SimApiAuth] 的接口:不添加安全要求,Swagger 不显示锁图标
}
}
}
@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SimApi.Attributes;
using SimApi.Communications;
namespace SimApi.SwaggerFilters;
public class SimApiResponseOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// 1. 跳过标记了 [OriginResponseAttribute] 的接口(不包装)
if (context.MethodInfo.DeclaringType?.GetCustomAttributes<OriginResponseAttribute>(true).Any() == true
|| context.MethodInfo.GetCustomAttributes<OriginResponseAttribute>(true).Any())
{
return;
}
// 2. 获取控制器方法声明的“原始返回类型”(如 Task<UserDto> → UserDto
var returnType = GetUnwrappedReturnType(context.MethodInfo.ReturnType, context);
if (IsAlreadyWrappedType(returnType))
{
return; // 保留原始返回类型的文档描述
}
// 3. 定义包装后的目标类型(泛型/非泛型)
Type wrappedType;
if (returnType == typeof(void) || returnType == typeof(EmptyResult))
{
// 无数据:用非泛型 SimApiBaseResponse
wrappedType = typeof(SimApiBaseResponse);
}
else
{
// 有数据:用泛型 SimApiBaseResponse<T>T 为原始返回类型)
wrappedType = typeof(SimApiBaseResponse<>).MakeGenericType(returnType);
}
// 4. 让 Swagger 生成包装类型的 Schema
var schema = context.SchemaGenerator.GenerateSchema(wrappedType, context.SchemaRepository);
// 5. 替换 Swagger 文档中的响应类型(只保留 200 OK 的响应,匹配过滤器逻辑)
operation.Responses.Clear(); // 清除默认响应(如 200 返回原始类型)
operation.Responses.Add("200", new OpenApiResponse
{
Description = "请求成功",
Content = new Dictionary<string, OpenApiMediaType>
{
{
"application/json", // 只保留 JSON 格式(配合之前的全局配置)
new OpenApiMediaType
{
Schema = schema
}
}
}
});
}
private static Type GetUnwrappedReturnType(Type returnType, OperationFilterContext context)
{
// 处理 Task<T>(异步方法)
if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>))
{
returnType = returnType.GetGenericArguments()[0];
}
// 处理 IActionResult/ObjectResult(如 return Ok(userDto)
if (!typeof(IActionResult).IsAssignableFrom(returnType)) return returnType;
// 从方法返回语句中提取原始类型(需结合特性,或默认用 object)
// 更精准的方式:让控制器方法用 [ProducesResponseType(typeof(UserDto), 200)] 声明原始类型
var producesAttr = context.MethodInfo.GetCustomAttributes<ProducesResponseTypeAttribute>()
.FirstOrDefault(a => a.StatusCode == 200);
if (producesAttr?.Type != null && producesAttr.Type != typeof(void))
{
return producesAttr.Type;
}
return typeof(object); // 无法识别时默认用 object
}
// 辅助方法:判断类型是否已经是 SimApiBaseResponse 及其泛型
private static bool IsAlreadyWrappedType(Type type)
{
// 情况1:类型本身是 SimApiBaseResponse 或其泛型
if (type == typeof(SimApiBaseResponse) ||
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(SimApiBaseResponse<>)))
{
return true;
}
// 情况2:类型是 SimApiBaseResponse 的子类(非泛型)
if (type.IsSubclassOf(typeof(SimApiBaseResponse)))
{
return true;
}
// 情况3:类型是 SimApiBaseResponse<T> 的子类(泛型)
// 检查是否继承自泛型父类 SimApiBaseResponse<>(任意 T
if (type.BaseType is { IsGenericType: true })
{
var genericBaseType = type.BaseType.GetGenericTypeDefinition();
if (genericBaseType == typeof(SimApiBaseResponse<>))
{
return true;
}
}
// 其他情况:不是包装类型或其子类
return false;
}
}
@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models;
using SimApi.Attributes;
using SimApi.ModelBinders;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace SimApi.SwaggerFilters;
public class SimApiSignOperationFilter(IServiceProvider serviceProvider) : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
// 1. 检查当前方法或类是否标注了 SimApiSignAttribute 及其子类
var signAttribute = context.MethodInfo.GetCustomAttribute<SimApiSignAttribute>(inherit: true)
?? context.MethodInfo.DeclaringType?.GetCustomAttribute<SimApiSignAttribute>(inherit: true);
if (signAttribute == null)
{
return;
}
var keyProviderType = signAttribute.KeyProvider;
if (!typeof(SimApiSignProviderBase).IsAssignableFrom(keyProviderType))
{
throw new InvalidOperationException($"KeyProvider 必须继承自 {nameof(SimApiSignProviderBase)}");
}
SimApiSignProviderBase? keyProvider;
try
{
keyProvider =
serviceProvider.CreateScope().ServiceProvider.GetService(keyProviderType) as SimApiSignProviderBase;
}
catch (Exception ex)
{
throw new InvalidOperationException($"无法从 DI 容器获取 {keyProviderType.Name} 实例:{ex.Message}");
}
if (keyProvider == null)
{
throw new InvalidOperationException($"{keyProviderType.Name} 未在 DI 容器中注册");
}
var signStr = keyProvider.SignFields.Aggregate(string.Empty, (current, field) => current + $"{field}=xxx&");
if (!string.IsNullOrEmpty(keyProvider.AppIdName))
{
signStr += $"{keyProvider.AppIdName}=xxx&";
}
signStr += $"{keyProvider.TimestampName}=xxx&{keyProvider.NonceName}=xxx&签名密钥";
var signParameters = new List<(string Name, string Description, bool Required)>
{
(keyProvider.TimestampName, "时间戳(秒级)", true),
(keyProvider.NonceName, "随机字符串", true),
(keyProvider.SignName, $"MD5签名结果,签名MD5字符串: {signStr}", true)
};
if (keyProvider.AppIdName != null)
{
signParameters.Add((keyProvider.AppIdName, "应用标识", true));
}
signParameters.AddRange(keyProvider.SignFields.Where(x => x != keyProvider.AppIdName)
.Select(f => (f, string.Empty, true)));
foreach (var (name, description, required) in signParameters)
{
if (operation.Parameters?.Any(p => p.Name == name) == true)
{
var tmp = operation.Parameters?.FirstOrDefault(p => p.Name == name);
if (tmp != null)
{
tmp.Required = required;
tmp.Description = description;
}
continue;
}
operation.Parameters ??= new List<OpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter
{
Name = name,
In = ParameterLocation.Query, // 指定为 Query 参数
Description = description,
Required = required,
Schema = new OpenApiSchema
{
Type = "string" // 签名相关参数通常为字符串类型
}
});
}
}
}