diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml
index 57c616d..424bb5f 100644
--- a/.github/workflows/nuget-publish.yml
+++ b/.github/workflows/nuget-publish.yml
@@ -18,7 +18,7 @@ jobs:
- name: Publish
run: |
version=`git describe --tags`
- dotnet build --configuration release -p:PackageVersion=$version
+ dotnet build --configuration release -p:Version=$version
dotnet nuget push bin/release/Simcu.SimApi.$version.nupkg -k ${NUGET_APIKEY} -s https://www.nuget.org/api/v2/package
env:
NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }}
diff --git a/Attributes/SimApiDocAttribute.cs b/Attributes/SimApiDocAttribute.cs
index 84a1171..c6d1999 100644
--- a/Attributes/SimApiDocAttribute.cs
+++ b/Attributes/SimApiDocAttribute.cs
@@ -16,7 +16,7 @@ public class SimApiDocAttribute : SwaggerOperationAttribute
/// 接口名称
public SimApiDocAttribute(string tag, string name)
{
- Tags = new[] { tag };
+ Tags = [tag];
Summary = name;
// Consumes = new[] {"application/json"};
// Produces = new[] {"application/json"};
diff --git a/Communications/SimApiBaseResponse.cs b/Communications/SimApiBaseResponse.cs
index ea0ff66..f55b7d7 100644
--- a/Communications/SimApiBaseResponse.cs
+++ b/Communications/SimApiBaseResponse.cs
@@ -13,6 +13,10 @@ public class SimApiBaseResponse(int code = 200, string message = "成功")
public int Code { get; set; } = code;
public string Message { get; set; } = message;
+ public SimApiBaseResponse() : this(200, "成功")
+ {
+ }
+
///
/// 默认错误代码对应提示信息
///
diff --git a/Configurations/SimApiOptions.cs b/Configurations/SimApiOptions.cs
index 9bd90d2..6bbf3cb 100644
--- a/Configurations/SimApiOptions.cs
+++ b/Configurations/SimApiOptions.cs
@@ -74,6 +74,14 @@ public class SimApiOptions
public bool EnableLowerUrl { get; set; } = true;
+ ///
+ /// 应用可以通过 /versions 显示出应用版本和SimApi包版本
+ /// default: true
+ ///
+ public bool EnableVersionUrl { get; set; } = true;
+
+
+
///
/// 启用格式化的 Console Logger
/// default: false
diff --git a/Controllers/SimApiBaseController.cs b/Controllers/SimApiBaseController.cs
index 37e4e30..6fc7093 100644
--- a/Controllers/SimApiBaseController.cs
+++ b/Controllers/SimApiBaseController.cs
@@ -14,6 +14,9 @@ namespace SimApi.Controllers;
/// 2. 报错返回
/// 3. 错误回馈页面
///
+///
+[Consumes("application/json")]
+[Produces("application/json")]
public class SimApiBaseController : Controller
{
///
diff --git a/Controllers/SimApiCommonController.cs b/Controllers/SimApiCommonController.cs
index 2dfbb4b..b74af01 100644
--- a/Controllers/SimApiCommonController.cs
+++ b/Controllers/SimApiCommonController.cs
@@ -1,9 +1,11 @@
-using Microsoft.AspNetCore.Mvc;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Mvc;
using SimApi.Attributes;
using SimApi.Communications;
using SimApi.Helpers;
namespace SimApi.Controllers;
+
public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController
{
///
@@ -25,7 +27,7 @@ public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController
[HttpPost, SimApiDoc("认证", "检测登陆")]
public SimApiBaseResponse CheckLogin()
{
- ErrorWhenNull(LoginInfo, 401,"未登录");
+ ErrorWhenNull(LoginInfo, 401, "未登录");
return new SimApiBaseResponse
{
Data = LoginInfo.Id
@@ -50,7 +52,20 @@ public class SimApiCommonController(SimApiAuth auth) : SimApiBaseController
return new SimApiBaseResponse();
}
- [HttpPost,SimApiAuth]
+ [HttpPost, HttpGet]
+ public SimApiBaseResponse> Versions()
+ {
+ return new SimApiBaseResponse>()
+ {
+ Data = new Dictionary
+ {
+ { "SimApi", SimApiUtil.SimApiVersion },
+ { "App", SimApiUtil.AppVersion }
+ }
+ };
+ }
+
+ [HttpPost, SimApiAuth]
public SimApiBaseResponse UserInfo()
{
return new SimApiBaseResponse(LoginInfo);
diff --git a/Helpers/SimApiAesUtil.cs b/Helpers/SimApiAesUtil.cs
new file mode 100644
index 0000000..f989b56
--- /dev/null
+++ b/Helpers/SimApiAesUtil.cs
@@ -0,0 +1,119 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace SimApi.Helpers;
+
+public static class SimApiAesUtil
+{
+ // 密钥长度:256位 (32字节)
+ private const int KeySize = 256;
+
+ // 块大小:128位 (16字节)
+ private const int BlockSize = 128;
+
+ // 加密模式 - 重命名以避免与枚举类型冲突
+ private const CipherMode AesCipherMode = CipherMode.CBC;
+
+ // 填充模式 - 重命名以避免与枚举类型冲突
+ private const PaddingMode AesPaddingMode = PaddingMode.PKCS7;
+
+ ///
+ /// AES 加密
+ ///
+ /// 明文
+ /// 字符串密钥(将被处理为256位)
+ /// 加密后的Base64字符串(包含IV)
+ public static string Encrypt(string plainText, string key)
+ {
+ if (string.IsNullOrEmpty(plainText))
+ throw new ArgumentNullException(nameof(plainText));
+ if (string.IsNullOrEmpty(key))
+ throw new ArgumentNullException(nameof(key));
+
+ // 处理密钥为指定长度
+ var keyBytes = ProcessKey(key);
+ // 生成随机IV
+ var iv = GenerateRandomIv();
+
+ using var aes = CreateAesProvider(keyBytes, iv);
+ using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
+ using var ms = new MemoryStream();
+ // 先写入IV,解密时需要用到
+ ms.Write(iv, 0, iv.Length);
+
+ using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
+ using (var sw = new StreamWriter(cs))
+ {
+ sw.Write(plainText);
+ }
+
+ return Convert.ToBase64String(ms.ToArray());
+ }
+
+ ///
+ /// AES 解密
+ ///
+ /// 加密后的Base64字符串
+ /// 字符串密钥(与加密时相同)
+ /// 解密后的明文
+ public static string Decrypt(string cipherText, string key)
+ {
+ if (string.IsNullOrEmpty(cipherText))
+ throw new ArgumentNullException(nameof(cipherText));
+ if (string.IsNullOrEmpty(key))
+ throw new ArgumentNullException(nameof(key));
+
+ var cipherBytes = Convert.FromBase64String(cipherText);
+
+ // 从加密数据中提取IV
+ var iv = new byte[BlockSize / 8];
+ Array.Copy(cipherBytes, 0, iv, 0, iv.Length);
+
+ // 处理密钥为指定长度
+ var keyBytes = ProcessKey(key);
+
+ using var aes = CreateAesProvider(keyBytes, iv);
+ using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
+ using var ms = new MemoryStream(cipherBytes, iv.Length, cipherBytes.Length - iv.Length);
+ using var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read);
+ using var sr = new StreamReader(cs);
+ return sr.ReadToEnd();
+ }
+
+ ///
+ /// 处理密钥为指定长度(256位)
+ ///
+ private static byte[] ProcessKey(string key)
+ {
+ // 使用SHA256哈希处理密钥,确保得到32字节(256位)的密钥
+ return SHA256.HashData(Encoding.UTF8.GetBytes(key));
+ }
+
+ ///
+ /// 生成随机初始化向量
+ ///
+ private static byte[] GenerateRandomIv()
+ {
+ using var aes = Aes.Create();
+ aes.BlockSize = BlockSize;
+ aes.GenerateIV();
+ return aes.IV;
+ }
+
+ ///
+ /// 创建并配置AES加密服务提供器
+ ///
+ private static Aes CreateAesProvider(byte[] key, byte[] iv)
+ {
+ var aes = Aes.Create();
+ aes.KeySize = KeySize;
+ aes.BlockSize = BlockSize;
+ aes.Mode = AesCipherMode;
+ aes.Padding = AesPaddingMode;
+ aes.Key = key;
+ aes.IV = iv;
+ return aes;
+ }
+}
\ No newline at end of file
diff --git a/Helpers/SimApiAuthOperationFilter.cs b/Helpers/SimApiAuthOperationFilter.cs
new file mode 100644
index 0000000..362c21b
--- /dev/null
+++ b/Helpers/SimApiAuthOperationFilter.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Microsoft.OpenApi.Models;
+using Swashbuckle.AspNetCore.SwaggerGen;
+using SimApi.Attributes;
+
+namespace SimApi.Helpers
+{
+ public class SimApiAuthOperationFilter : IOperationFilter
+ {
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ // 检查接口或控制器是否标记了 [SimApiAuth] 特性
+ var requiresAuth =
+ // 方法上有 [SimApiAuth]
+ context.MethodInfo.GetCustomAttributes(true).Any()
+ ||
+ // 控制器上有 [SimApiAuth](继承到所有方法)
+ context.MethodInfo.DeclaringType?.GetCustomAttributes(true).Any() == true;
+ if (requiresAuth)
+ {
+ // 添加授权要求:关联步骤 2 中定义的 "SimApiAuth" 安全方案
+ operation.Security = new List
+ {
+ new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = "SimApiAuth" // 必须与 AddSecurityDefinition 的第一个参数一致
+ }
+ },
+ [] // 无需指定作用域(scope)时留空
+ }
+ }
+ };
+ }
+ // 未标记 [SimApiAuth] 的接口:不添加安全要求,Swagger 不显示锁图标
+ }
+ }
+}
\ No newline at end of file
diff --git a/Helpers/SimApiUtil.cs b/Helpers/SimApiUtil.cs
index af35990..4ad7298 100644
--- a/Helpers/SimApiUtil.cs
+++ b/Helpers/SimApiUtil.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Encodings.Web;
@@ -30,6 +31,53 @@ public static class SimApiUtil
// DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
+ public static string SimApiVersion
+ {
+ get
+ {
+ // 这里使用当前类(属于 NuGet 包)的程序集
+ var assembly = typeof(SimApiUtil).Assembly;
+
+ // 优先获取 AssemblyInformationalVersion(通常对应 NuGet 包版本,可能包含预发布标签)
+ var informationalVersion =
+ assembly.GetCustomAttribute()?.InformationalVersion;
+ if (!string.IsNullOrEmpty(informationalVersion))
+ {
+ return informationalVersion;
+ }
+
+ // 若不存在,则获取 AssemblyVersion(编译时版本)
+ var version = assembly.GetName().Version?.ToString();
+ return version ?? "Unknown";
+ }
+ }
+
+ public static string AppVersion
+ {
+ get
+ {
+ // 获取外层应用的入口程序集(通常是启动项目的程序集)
+ var entryAssembly = Assembly.GetEntryAssembly();
+ if (entryAssembly == null)
+ {
+ // 特殊场景(如单元测试、某些宿主环境)下,入口程序集可能为 null,可尝试获取调用栈中的上层程序集
+ entryAssembly = Assembly.GetCallingAssembly(); // 或 Assembly.GetExecutingAssembly() 视场景调整
+ }
+
+ // 优先获取应用的 AssemblyInformationalVersion
+ var informationalVersion = entryAssembly.GetCustomAttribute()
+ ?.InformationalVersion;
+ if (!string.IsNullOrEmpty(informationalVersion))
+ {
+ return informationalVersion;
+ }
+
+ // 若不存在,则获取 AssemblyVersion
+ var version = entryAssembly.GetName().Version?.ToString();
+ return version ?? "Unknown";
+ }
+ }
+
///
/// 当前秒级时间戳
///
diff --git a/Helpers/WrapResponseSchemaFilter.cs b/Helpers/WrapResponseSchemaFilter.cs
new file mode 100644
index 0000000..d6aa856
--- /dev/null
+++ b/Helpers/WrapResponseSchemaFilter.cs
@@ -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.Helpers;
+
+public class WrapResponseSchemaFilter : IOperationFilter
+{
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ // 1. 跳过标记了 [OriginResponseAttribute] 的接口(不包装)
+ if (context.MethodInfo.DeclaringType?.GetCustomAttributes(true).Any() == true
+ || context.MethodInfo.GetCustomAttributes(true).Any())
+ {
+ return;
+ }
+
+ // 2. 获取控制器方法声明的“原始返回类型”(如 Task → 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 为原始返回类型)
+ 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
+ {
+ {
+ "application/json", // 只保留 JSON 格式(配合之前的全局配置)
+ new OpenApiMediaType
+ {
+ Schema = schema
+ }
+ }
+ }
+ });
+ }
+
+ private static Type GetUnwrappedReturnType(Type returnType, OperationFilterContext context)
+ {
+ // 处理 Task(异步方法)
+ 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()
+ .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 的子类(泛型)
+ // 检查是否继承自泛型父类 SimApiBaseResponse<>(任意 T)
+ if (type.BaseType is { IsGenericType: true })
+ {
+ var genericBaseType = type.BaseType.GetGenericTypeDefinition();
+ if (genericBaseType == typeof(SimApiBaseResponse<>))
+ {
+ return true;
+ }
+ }
+
+ // 其他情况:不是包装类型或其子类
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/SimApi.csproj b/SimApi.csproj
index f515b53..e5c9769 100644
--- a/SimApi.csproj
+++ b/SimApi.csproj
@@ -3,17 +3,10 @@
Library
true
- 0.2.3
+ 0.0.0
xRain@SimcuTeam
AspNetCore一个方便的API文档,捕获异常,统一输入输出的API类库
Simcu.SimApi
- true
- snupkg
- true
- 5.0.0
- false
- true
- 5.0.2
enable
net8.0;net9.0
@@ -23,15 +16,15 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/SimApiExtensions.cs b/SimApiExtensions.cs
index 7a1a22f..2ea8db1 100644
--- a/SimApiExtensions.cs
+++ b/SimApiExtensions.cs
@@ -121,30 +121,27 @@ public static class SimApiExtensions
});
}
+ x.CustomSchemaIds(type => type.FullName?.Replace("+", "."));
+ x.OperationFilter();
+ if (simApiOptions.EnableSimApiAuth)
+ {
+ x.OperationFilter();
+ }
+
x.EnableAnnotations();
var haveOauth = false;
- var haveSimApiAuth = false;
var oauthFlows = new OpenApiOAuthFlows();
foreach (var auth in docOptions.ApiAuth.Type)
{
switch (auth)
{
case "SimApiAuth":
- x.AddSecurityRequirement(new OpenApiSecurityRequirement
- {
+ x.AddSecurityDefinition("HeaderToken",
+ new OpenApiSecurityScheme
{
- new OpenApiSecurityScheme
- {
- Reference = new OpenApiReference
- {
- Type = ReferenceType.SecurityScheme,
- Id = "HeaderToken"
- }
- },
- new[] { "readAccess", "writeAccess" }
- }
- });
- haveSimApiAuth = true;
+ Name = "Token",
+ In = ParameterLocation.Header
+ });
break;
case "ClientCredentials":
oauthFlows.ClientCredentials = new OpenApiOAuthFlow
@@ -193,30 +190,6 @@ public static class SimApiExtensions
Description = docOptions.ApiAuth.Description,
In = ParameterLocation.Header
});
- x.AddSecurityRequirement(new OpenApiSecurityRequirement
- {
- {
- new OpenApiSecurityScheme
- {
- Reference = new OpenApiReference
- {
- Type = ReferenceType.SecurityScheme,
- Id = "oauth2"
- }
- },
- ["SimApiAuth"]
- }
- });
- }
-
- if (haveSimApiAuth)
- {
- x.AddSecurityDefinition("HeaderToken",
- new OpenApiSecurityScheme
- {
- Name = "Token",
- In = ParameterLocation.Header
- });
}
});
}
@@ -264,6 +237,8 @@ public static class SimApiExtensions
var logger = builder.Services.GetRequiredService>();
logger.LogInformation("当前时区: {LocalId}", TimeZoneInfo.Local.Id);
+ logger.LogInformation("主应用版本: {AppVersion}\nSimApi版本: {SimApiVersion}", SimApiUtil.AppVersion,
+ SimApiUtil.SimApiVersion);
if (options.RedisConfiguration != null)
{
@@ -374,22 +349,33 @@ public static class SimApiExtensions
}
}
+ if (options.EnableVersionUrl)
+ {
+ builder.MapControllerRoute(name: "Versions", pattern: "/versions",
+ defaults: new
+ {
+ controller = "SimApiCommon",
+ action = "Versions"
+ });
+ }
+
if (options.EnableSimApiDoc)
{
logger.LogInformation("开始配置SimApiDoc...");
var docOptions = options.SimApiDocOptions;
- builder.UseSwagger(x => x.RouteTemplate = "/swagger/{documentName}.json").UseSwaggerUI(x =>
- {
- x.DocumentTitle = docOptions.DocumentTitle;
- foreach (var group in docOptions.ApiGroups)
+ builder.UseSwagger(x => x.RouteTemplate = "/swagger/{documentName}.json")
+ .UseSwaggerUI(x =>
{
- x.SwaggerEndpoint($"/swagger/{group.Id}.json", name: group.Name);
- }
+ x.DocumentTitle = docOptions.DocumentTitle;
+ foreach (var group in docOptions.ApiGroups)
+ {
+ x.SwaggerEndpoint($"/swagger/{group.Id}.json", name: group.Name);
+ }
- x.EnableValidator();
- x.SupportedSubmitMethods(docOptions.SupportedMethod);
- x.DisplayRequestDuration();
- });
+ x.EnableValidator();
+ x.SupportedSubmitMethods(docOptions.SupportedMethod);
+ x.DisplayRequestDuration();
+ });
}
if (options.EnableSimApiException)