Compare commits

..
20 Commits
Author SHA1 Message Date
xrain 28c6d53f38 修正了文档,增加了SimApiError 2026-04-26 10:03:38 +08:00
xrain ebceb0a260 Error系列全局可用 2026-04-26 04:09:35 +08:00
xrain 162b92a415 增加了cache.Remove 2026-04-26 01:23:14 +08:00
xrain a859986a93 fix csproj 2026-04-24 09:29:02 +08:00
xrain e1c6694e18 fix ex aggr 2026-04-14 13:38:19 +08:00
xrain 566b1d190b add AiContext 2026-04-07 10:55:46 +08:00
xrain eb8c9732bd 修改了说明书 2026-04-07 10:44:34 +08:00
xrain c8b78ce86d add tags for doc 2026-03-26 00:52:52 +08:00
xrain b2ec94e957 fix ci 2026-03-26 00:41:55 +08:00
xrain a8439c174c SimApiDoc增加了描述 2026-03-26 00:32:20 +08:00
xrain 81e166ad45 修正了swgger生成的类型名,SimApiAuth 如果不加参数,则默认接受所有登录了的用户请求. 2025-12-23 08:18:11 +08:00
xrain d50d32f032 优化了文档中的类型名称,修复了认证接口不显示需要认证 2025-12-23 07:20:34 +08:00
xrain 8ad43526fc 修正了文档认证不显示的问题 2025-12-23 06:00:33 +08:00
xrain e2d30c031d 隐藏了文档中空Tag, 禁用了文档远程校验 2025-12-19 13:37:58 +08:00
xrain 9dc4ef7bce http client 增加了DebugLog 2025-12-18 16:17:18 +08:00
xrain d5eb7fe081 分页修改了,不在使用单独的分页返回,httpclient 不需要simaporesponse的包装,报错直接抛出异常 2025-12-18 14:06:11 +08:00
xrain b04a7c86d7 signQuery body defaultNull 2025-12-18 13:05:05 +08:00
xrain 8759b593db SimApiHttpClientAddServer 2025-12-18 11:28:47 +08:00
xrainandGitHub 5a46a026a7 Update nuget-publish.yml
fix ci
2025-12-18 09:13:19 +08:00
xrain 90124b6e67 update v10 2025-12-18 09:08:35 +08:00
21 changed files with 1679 additions and 251 deletions
+11 -6
View File
@@ -9,16 +9,21 @@ jobs:
publish: publish:
name: Publish Project to Nuget name: Publish Project to Nuget
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
id-token: write
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v4
- name: Setup .NET Core - name: Setup .NET Core
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: "9.0.305" dotnet-version: "10.x.x"
- name: NuGet login (OIDC → temp API key)
uses: NuGet/login@v1
id: login
with:
user: ${{ secrets.NUGET_USER }}
- name: Publish - name: Publish
run: | run: |
version=`git describe --tags` version=`git describe --tags`
dotnet build --configuration release -p:Version=$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 dotnet nuget push bin/release/Simcu.SimApi.$version.nupkg -k ${{steps.login.outputs.NUGET_API_KEY}} -s https://api.nuget.org/v3/index.json
env:
NUGET_APIKEY: ${{ secrets.NUGET_APIKEY }}
+4 -9
View File
@@ -13,13 +13,13 @@ namespace SimApi.Attributes;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SimApiAuthAttribute : ActionFilterAttribute public class SimApiAuthAttribute : ActionFilterAttribute
{ {
private string[] Types { get; } private string[]? Types { get; }
//默认是user登录类型 //默认是user登录类型
public SimApiAuthAttribute() public SimApiAuthAttribute()
{ {
Types = new[] { "user" }; Types = null;
} }
//只检测一种用户类型的快捷方式 //只检测一种用户类型的快捷方式
@@ -34,13 +34,6 @@ public class SimApiAuthAttribute : ActionFilterAttribute
Types = types; Types = types;
} }
//只检测一种用户类型的快捷方式
public SimApiAuthAttribute(string type, string url)
{
Types = new[] { type };
new HttpPostAttribute(url);
}
public override void OnActionExecuting(ActionExecutingContext context) public override void OnActionExecuting(ActionExecutingContext context)
{ {
var loginInfo = (SimApiLoginItem)context.HttpContext.Items["LoginInfo"]!; var loginInfo = (SimApiLoginItem)context.HttpContext.Items["LoginInfo"]!;
@@ -49,6 +42,8 @@ public class SimApiAuthAttribute : ActionFilterAttribute
{ {
throw new SimApiException(401); throw new SimApiException(401);
} }
if (Types == null) return;
//检测用户类型 //检测用户类型
if (!Types.Intersect(loginInfo.Type).Any()) if (!Types.Intersect(loginInfo.Type).Any())
{ {
+18 -3
View File
@@ -12,13 +12,28 @@ public class SimApiDocAttribute : SwaggerOperationAttribute
/// <summary> /// <summary>
/// 定义接口说明 /// 定义接口说明
/// </summary> /// </summary>
/// <param name="tag">接口分组</param> /// <param name="tags">接口分组列表</param>
/// <param name="name">接口名称</param> /// <param name="name">接口名称</param>
public SimApiDocAttribute(string tag, string name) /// <param name="description">接口描述</param>
public SimApiDocAttribute(string[] tags, string name, string? description = null)
{ {
Tags = [tag]; Tags = tags;
Summary = name; Summary = name;
if (description != null)
{
Description = description;
}
// Consumes = new[] {"application/json"}; // Consumes = new[] {"application/json"};
// Produces = new[] {"application/json"}; // Produces = new[] {"application/json"};
} }
/// <summary>
/// 定义接口说明
/// </summary>
/// <param name="tag">接口分组</param>
/// <param name="name">接口名称</param>
/// <param name="description">接口描述</param>
public SimApiDocAttribute(string tag, string name, string? description = null) : this([tag], name, description)
{
}
} }
+1
View File
@@ -7,6 +7,7 @@ using SimApi.Attributes;
using SimApi.Communications; using SimApi.Communications;
using SimApi.Controllers; using SimApi.Controllers;
using SimApi.Helpers; using SimApi.Helpers;
using static SimApi.Helpers.SimApiError;
namespace SimApi.CoceSdk; namespace SimApi.CoceSdk;
+2 -10
View File
@@ -46,23 +46,15 @@ public class SimApiBaseResponse(int code = 200, string message = "成功")
} }
/// <summary> /// <summary>
/// 动态内容分页 /// 分页内容返回
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
public class SimApiBasePageResponse<T>() : SimApiBaseResponse public class PageResponse<T>
{ {
public T? List { get; set; } public T? List { get; set; }
public int Page { get; set; } = 1; public int Page { get; set; } = 1;
public int Count { get; set; } = 20; public int Count { get; set; } = 20;
public int Total { get; set; } public int Total { get; set; }
public SimApiBasePageResponse(T list, int page, int count, int total) : this()
{
List = list;
Page = page;
Count = count;
Total = total;
}
} }
/// <summary> /// <summary>
+2 -1
View File
@@ -29,11 +29,12 @@ public class SimApiJobOptions
/// 设置为null 使用默认redis配置 /// 设置为null 使用默认redis配置
/// </summary> /// </summary>
public int? Database { get; set; } = null; public int? Database { get; set; } = null;
public SimApiJobServerConfig[] Servers { get; set; } = [new()]; public SimApiJobServerConfig[] Servers { get; set; } = [new()];
} }
public class SimApiJobServerConfig() public class SimApiJobServerConfig()
{ {
public string[] Queues { get; set; } = ["default"]; public string[] Queues { get; set; } = ["default"];
public int WorkerNum { get; set; } = 50; public int WorkerNum { get; set; } = 5;
} }
+4 -2
View File
@@ -1,11 +1,12 @@
using System.Collections.Generic; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using SimApi.Attributes; using SimApi.Attributes;
using SimApi.Communications; using SimApi.Communications;
using SimApi.Helpers; using SimApi.Helpers;
namespace SimApi.Controllers; namespace SimApi.Controllers;
using static SimApiError;
public class SimApiAuthController(SimApiAuth auth) : SimApiBaseController public class SimApiAuthController(SimApiAuth auth) : SimApiBaseController
{ {
/// <summary> /// <summary>
@@ -29,6 +30,7 @@ public class SimApiAuthController(SimApiAuth auth) : SimApiBaseController
[HttpPost, SimApiDoc("认证", "退出登陆")] [HttpPost, SimApiDoc("认证", "退出登陆")]
public SimApiBaseResponse Logout() public SimApiBaseResponse Logout()
{ {
ErrorWhenNull(null);
string? token = null; string? token = null;
if (Request.Headers.TryGetValue("Token", out var value)) if (Request.Headers.TryGetValue("Token", out var value))
+2 -73
View File
@@ -1,10 +1,9 @@
using System.Diagnostics.CodeAnalysis; using SimApi.Communications;
using SimApi.Communications;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using System.Linq; using System.Linq;
using SimApi.Exceptions; using static SimApi.Helpers.SimApiError;
namespace SimApi.Controllers; namespace SimApi.Controllers;
@@ -38,74 +37,4 @@ public class SimApiBaseController : Controller
break; break;
} }
} }
/// <summary>
/// 错误返回
/// </summary>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述(若是常规错误,代码可自动带取描述)</param>
/// <returns></returns>
protected static void Error(int code = 500, string message = "")
{
throw new SimApiException(code, message);
}
/// <summary>
/// 检测条件,根据条件返回报错 如果condition是ture报错
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
protected static void ErrorWhen([DoesNotReturnIf(true)] bool condition, int code = 400, string message = "")
{
if (condition)
{
Error(code, message);
}
}
/// <summary>
/// 检测条件,根据条件返回报错 如果condition是ture报错
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
protected static void ErrorWhenTrue([DoesNotReturnIf(true)] bool condition, int code = 400, string message = "")
{
ErrorWhen(condition, code, message);
}
/// <summary>
/// 如果condition是false 报错
/// </summary>
/// <param name="condition"></param>
/// <param name="code"></param>
/// <param name="message"></param>
protected static void ErrorWhenFalse([DoesNotReturnIf(false)] bool condition, int code = 400, string message = "")
{
ErrorWhen(!condition, code, message);
}
/// <summary>
/// 检测给定的变量是否为NUll
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
protected static void ErrorWhenNull([NotNull] object? condition, int code = 404, string message = "请求的资源不存在")
{
ErrorWhen(condition == null, code, message);
}
/// <summary>
/// 上传文件
/// </summary>
/// <returns></returns>
protected SimApiBaseResponse<string> UploadFile()
{
return new SimApiBaseResponse<string>();
}
} }
+26
View File
@@ -7,6 +7,12 @@ public class SimApiCache(IDistributedCache cache)
{ {
private const string Prefix = "SimApi:Cache:"; private const string Prefix = "SimApi:Cache:";
/// <summary>
/// 设置缓存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="options"></param>
public void Set(string key, object value, DistributedCacheEntryOptions? options = null) public void Set(string key, object value, DistributedCacheEntryOptions? options = null)
{ {
if (options is not null) if (options is not null)
@@ -19,11 +25,31 @@ public class SimApiCache(IDistributedCache cache)
} }
} }
/// <summary>
/// 移除缓存
/// </summary>
/// <param name="key"></param>
public void Remove(string key)
{
cache.Remove(Prefix + key);
}
/// <summary>
/// 获取string类型缓存
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public string? Get(string key) public string? Get(string key)
{ {
return cache.GetString(Prefix + key); return cache.GetString(Prefix + key);
} }
/// <summary>
/// 获取特定类型缓存
/// </summary>
/// <param name="key"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T? Get<T>(string key) public T? Get<T>(string key)
{ {
var data = cache.GetString(Prefix + key); var data = cache.GetString(Prefix + key);
+83
View File
@@ -0,0 +1,83 @@
using System.Diagnostics.CodeAnalysis;
using SimApi.Exceptions;
namespace SimApi.Helpers;
public static class SimApiError
{
/// <summary>
/// 错误返回
/// </summary>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述(若是常规错误,代码可自动带取描述)</param>
/// <returns></returns>
public static void Error(int code = 500, string message = "")
{
throw new SimApiException(code, message);
}
/// <summary>
/// 检测条件,根据条件返回报错 如果condition是ture报错
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
public static void ErrorWhen([DoesNotReturnIf(true)] bool condition, int code = 400,
string message = "")
{
if (condition)
{
Error(code, message);
}
}
/// <summary>
/// 检测条件,根据条件返回报错 如果condition是ture报错
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
public static void ErrorWhenTrue([DoesNotReturnIf(true)] bool condition, int code = 400,
string message = "")
{
ErrorWhen(condition, code, message);
}
/// <summary>
/// 如果condition是false 报错
/// </summary>
/// <param name="condition"></param>
/// <param name="code"></param>
/// <param name="message"></param>
public static void ErrorWhenFalse([DoesNotReturnIf(false)] bool condition, int code = 400,
string message = "")
{
ErrorWhen(!condition, code, message);
}
/// <summary>
/// 检测给定的变量是否为NUll
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
public static void ErrorWhenNull([NotNull] object? condition, int code = 404,
string message = "请求的资源不存在")
{
ErrorWhen(condition == null, code, message);
}
/// <summary>
/// 检测给定的泛型变量是否为Null(提供更精确的编译器 null 流分析)
/// </summary>
/// <param name="condition">检测条件</param>
/// <param name="code">错误代码</param>
/// <param name="message">错误描述</param>
public static void ErrorWhenNull<T>([NotNull] T? condition, int code = 404,
string message = "请求的资源不存在") where T : class
{
ErrorWhen(condition == null, code, message);
}
}
+62 -9
View File
@@ -4,20 +4,30 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json; using System.Net.Http.Json;
using SimApi.Communications; using SimApi.Communications;
using SimApi.Exceptions;
namespace SimApi.Helpers; namespace SimApi.Helpers;
public class SimApiHttpClient(string? appId, string appKey) public class SimApiHttpClient(string? appId, string appKey, bool debug = false)
{ {
public string Server { get; init; } = string.Empty;
public string SignName { get; init; } = "sign"; public string SignName { get; init; } = "sign";
public string TimestampName { get; init; } = "timestamp"; public string TimestampName { get; init; } = "timestamp";
public string NonceName { get; init; } = "nonce"; public string NonceName { get; init; } = "nonce";
public string? AppIdName { get; init; } = "appId"; public string? AppIdName { get; init; } = "appId";
public string[] SignFields { get; init; } = []; public string[] SignFields { get; init; } = [];
/// <summary>
public T? SignQuery<T>(string url, object body, Dictionary<string, string>? queries = null) /// 发起签名请求
/// </summary>
/// <param name="url"></param>
/// <param name="body"></param>
/// <param name="queries"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T? SignQuery<T>(string url, object? body = null, Dictionary<string, string>? queries = null)
{ {
url = Server + url;
var queryUrl = SignFields.Aggregate(string.Empty, var queryUrl = SignFields.Aggregate(string.Empty,
(current, signField) => current + $"{signField}={queries?[signField]}&"); (current, signField) => current + $"{signField}={queries?[signField]}&");
if (!string.IsNullOrEmpty(AppIdName)) if (!string.IsNullOrEmpty(AppIdName))
@@ -35,13 +45,19 @@ public class SimApiHttpClient(string? appId, string appKey)
.Aggregate(path, (current, q) => current + $"&{q.Key}={q.Value}"); .Aggregate(path, (current, q) => current + $"&{q.Key}={q.Value}");
} }
var http = new HttpClient(); return Query<T>(path, body);
var resp = http.PostAsJsonAsync(path, body).Result;
return resp.Content.ReadFromJsonAsync<T>().Result;
} }
/// <summary>
/// 发起AES加密请求
/// </summary>
/// <param name="url"></param>
/// <param name="body"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T? AesQuery<T>(string url, object body) public T? AesQuery<T>(string url, object body)
{ {
url = Server + url;
if (!string.IsNullOrEmpty(AppIdName)) if (!string.IsNullOrEmpty(AppIdName))
{ {
url += $"?{AppIdName}={appId}"; url += $"?{AppIdName}={appId}";
@@ -51,11 +67,17 @@ public class SimApiHttpClient(string? appId, string appKey)
{ {
Data = SimApiAesUtil.Encrypt(SimApiUtil.Json(body), appKey) Data = SimApiAesUtil.Encrypt(SimApiUtil.Json(body), appKey)
}; };
var http = new HttpClient(); return Query<T>(url, req);
var resp = http.PostAsJsonAsync(url, req).Result;
return resp.Content.ReadFromJsonAsync<T>().Result;
} }
/// <summary>
/// 发起AES加密以及签名请求
/// </summary>
/// <param name="url"></param>
/// <param name="body"></param>
/// <param name="queries"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T? AesSignQuery<T>(string url, object body, Dictionary<string, string>? queries = null) public T? AesSignQuery<T>(string url, object body, Dictionary<string, string>? queries = null)
{ {
var req = new SimApiOneFieldRequest<string> var req = new SimApiOneFieldRequest<string>
@@ -64,4 +86,35 @@ public class SimApiHttpClient(string? appId, string appKey)
}; };
return SignQuery<T>(url, req, queries); return SignQuery<T>(url, req, queries);
} }
/// <summary>
/// 发起请求
/// </summary>
/// <param name="url"></param>
/// <param name="req"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="SimApiException"></exception>
private T? Query<T>(string url, object? req)
{
var http = new HttpClient();
if (debug)
{
Console.WriteLine($"[HTTPCLIENT请求] {url}\n{SimApiUtil.Json(req)}\n");
}
var resp = http.PostAsJsonAsync(url, req).Result;
if (debug)
{
Console.WriteLine($"[HTTPCLIENT响应] {resp.Content.ReadAsStringAsync().Result}\n");
}
var res = resp.Content.ReadFromJsonAsync<SimApiBaseResponse<T>>().Result;
if (res == null)
{
throw new SimApiException(500, "请求发生错误");
}
return res.Code != 200 ? throw new SimApiException(res.Code, res.Message) : res.Data;
}
} }
+36 -21
View File
@@ -1,5 +1,4 @@
using System; using System;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using SimApi.Communications; using SimApi.Communications;
@@ -20,7 +19,6 @@ public class SimApiExceptionMiddleware(RequestDelegate next, ILogger<SimApiExcep
context.Response.Headers["Query-Id"] = header; context.Response.Headers["Query-Id"] = header;
} }
SimApiBaseResponse response;
try try
{ {
await next(context); await next(context);
@@ -39,31 +37,48 @@ public class SimApiExceptionMiddleware(RequestDelegate next, ILogger<SimApiExcep
} }
} }
} }
catch (SimApiException ex)
{
response = string.IsNullOrEmpty(ex.Message)
? new SimApiBaseResponse(ex.Code)
: new SimApiBaseResponse(ex.Code, ex.Message);
ErrorResponse(context, response);
}
catch (Exception ex) catch (Exception ex)
{ {
log.LogError("{Msg}", ex.Message); // 解包异步异常
log.LogError("{Msg}", ex.StackTrace); ex = UnwrapAggregateException(ex);
response = new SimApiBaseResponse(500, ex.Message); SimApiBaseResponse response;
ErrorResponse(context, response); if (ex is SimApiException simEx)
{
response = string.IsNullOrEmpty(simEx.Message)
? new SimApiBaseResponse(simEx.Code)
: new SimApiBaseResponse(simEx.Code, simEx.Message);
}
else
{
log.LogError(ex, "服务器异常");
response = new SimApiBaseResponse(500, "服务器错误");
}
await ErrorResponseAsync(context, response);
} }
} }
/// <summary> private static Exception UnwrapAggregateException(Exception ex)
/// 异常抛出错误
/// </summary>
/// <param name="context"></param>
/// <param name="response"></param>
private static void ErrorResponse(HttpContext context, SimApiBaseResponse response)
{ {
while (ex is AggregateException aggEx && aggEx.InnerException != null)
{
ex = aggEx.InnerException;
}
return ex;
}
/// <summary>
/// 异步输出错误响应(修复异步异常捕获核心)
/// </summary>
private static async Task ErrorResponseAsync(HttpContext context, SimApiBaseResponse response)
{
// 响应已开始则直接返回,不修改
if (context.Response.HasStarted)
return;
context.Response.StatusCode = 200; context.Response.StatusCode = 200;
context.Response.Headers.Append("Content-Type", "application/json"); context.Response.ContentType = "application/json";
context.Response.WriteAsync(response.ToString()).Wait(); await context.Response.WriteAsync(response.ToString());
} }
} }
+1275 -41
View File
File diff suppressed because it is too large Load Diff
+4 -18
View File
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<PackOnBuild>true</PackOnBuild> <PackOnBuild>true</PackOnBuild>
@@ -9,30 +8,17 @@
<Description>AspNetCore一个方便的API文档,捕获异常,统一输入输出的API类库</Description> <Description>AspNetCore一个方便的API文档,捕获异常,统一输入输出的API类库</Description>
<PackageId>Simcu.SimApi</PackageId> <PackageId>Simcu.SimApi</PackageId>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<Folder Include="Communications\"/>
<Folder Include="Exceptions\"/>
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.22" /> <PackageReference Include="Hangfire.AspNetCore" Version="1.8.22" />
<PackageReference Include="Hangfire.Console" Version="1.4.3"/> <PackageReference Include="Hangfire.Console" Version="1.4.3"/>
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.12.0"/> <PackageReference Include="Hangfire.Redis.StackExchange" Version="1.12.0"/>
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.10" /> <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.1" />
<PackageReference Include="Minio" Version="7.0.0" /> <PackageReference Include="Minio" Version="7.0.0" />
<PackageReference Include="MQTTnet" Version="5.0.1.1416"/> <PackageReference Include="MQTTnet" Version="5.0.1.1416"/>
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="9.0.6" /> <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.6" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.1.0" />
</ItemGroup> </ItemGroup>
<ProjectExtensions>
<MonoDevelop>
<Properties>
<Policies>
<DotNetNamingPolicy ResourceNamePolicy="FileFormatDefault" DirectoryNamespaceAssociation="PrefixedHierarchical"/>
</Policies>
</Properties>
</MonoDevelop>
</ProjectExtensions>
</Project> </Project>
+74 -5
View File
@@ -9,7 +9,7 @@ using Hangfire.Redis.StackExchange;
using SimApi.Helpers; using SimApi.Helpers;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi;
using SimApi.Middlewares; using SimApi.Middlewares;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -123,11 +123,80 @@ public static class SimApiExtensions
}); });
} }
x.CustomSchemaIds(type => type.FullName?.Replace("+", ".")); x.CustomSchemaIds(type =>
{
// 递归解析类型名称(处理嵌套泛型/数组/可空 + 保证唯一性)
string GetSimpleTypeName(Type t, int depth = 0)
{
// 防止无限递归
if (depth > 5) return t.Name.Split('`')[0];
// 处理数组类型
if (t.IsArray)
{
var elementType = t.GetElementType();
return $"{GetSimpleTypeName(elementType!, depth + 1)}[]";
}
// 处理可空类型
if (Nullable.GetUnderlyingType(t) != null)
{
var underlyingType = Nullable.GetUnderlyingType(t);
return GetSimpleTypeName(underlyingType!, depth + 1);
}
// 处理泛型类型(递归解析嵌套泛型)
if (t.IsGenericType)
{
var genericBaseName = t.GetGenericTypeDefinition().Name.Split('`')[0];
var genericArgs = t.GetGenericArguments()
.Select(arg => GetSimpleTypeName(arg, depth + 1))
.Where(arg => !string.IsNullOrEmpty(arg))
.ToArray();
return $"{genericBaseName}<{string.Join(",", genericArgs)}>";
}
// 处理基础类型(小写)
if (t.IsPrimitive || t == typeof(string) || t == typeof(DateTime) || t == typeof(Guid))
{
return t.Name switch
{
"String" => "string",
"Int32" => "int",
"Int64" => "long",
"Boolean" => "boolean",
"DateTime" => "datetime",
"Guid" => "guid",
_ => t.Name.ToLower()
};
}
// 核心修复:生成唯一名称(处理嵌套类/同名不同类)
var typeName = t.Name;
// 步骤1:处理嵌套类(如 ApplicationDto+ApplicationEditRequest → ApplicationDto_ApplicationEditRequest
if (t.DeclaringType != null)
{
typeName = $"{GetSimpleTypeName(t.DeclaringType)}.{typeName}";
}
// 步骤2:(可选)处理同命名空间下的同名类(拼接命名空间前缀,避免全局重复)
// 如需更严格的唯一性,取消注释下面这行
// typeName = $"{t.Namespace?.Replace(".", "_")}_{typeName}";
return typeName;
}
// 根调用:解析当前类型
var uniqueSchemaId = GetSimpleTypeName(type);
// 可选:移除特殊字符(如 $、+),避免 Swagger 解析问题
return uniqueSchemaId.Replace("$", "").Replace("+", "_");
});
x.OperationFilter<SimApiResponseOperationFilter>(); x.OperationFilter<SimApiResponseOperationFilter>();
x.OperationFilter<SimApiSignOperationFilter>(); x.OperationFilter<SimApiSignOperationFilter>();
x.OperationFilter<AesBodyOperationFilter>(); x.OperationFilter<AesBodyOperationFilter>();
x.SchemaFilter<GlobalDynamicObjectSchemaFilter>(); x.SchemaFilter<GlobalDynamicObjectSchemaFilter>();
x.DocumentFilter<RemoveEmptyTagsFilter>();
if (simApiOptions.EnableSimApiAuth) if (simApiOptions.EnableSimApiAuth)
{ {
x.OperationFilter<SimApiAuthOperationFilter>(); x.OperationFilter<SimApiAuthOperationFilter>();
@@ -157,11 +226,12 @@ public static class SimApiExtensions
switch (auth) switch (auth)
{ {
case "SimApiAuth": case "SimApiAuth":
x.AddSecurityDefinition("HeaderToken", x.AddSecurityDefinition("SimApiAuth",
new OpenApiSecurityScheme new OpenApiSecurityScheme
{ {
Name = "Token", Name = "Token",
In = ParameterLocation.Header In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
}); });
break; break;
case "ClientCredentials": case "ClientCredentials":
@@ -392,7 +462,6 @@ public static class SimApiExtensions
x.SwaggerEndpoint($"/swagger/{group.Id}.json", name: group.Name); x.SwaggerEndpoint($"/swagger/{group.Id}.json", name: group.Name);
} }
x.EnableValidator();
x.SupportedSubmitMethods(docOptions.SupportedMethod); x.SupportedSubmitMethods(docOptions.SupportedMethod);
x.DisplayRequestDuration(); x.DisplayRequestDuration();
}); });
+4 -3
View File
@@ -1,10 +1,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.OpenApi;
using SimApi.Attributes; using SimApi.Attributes;
namespace SimApi.SwaggerFilters; namespace SimApi.SwaggerFilters;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection; using System.Reflection;
@@ -21,11 +22,11 @@ public class AesBodyOperationFilter : IOperationFilter
.GetCustomAttribute<AesBodyAttribute>() != null; .GetCustomAttribute<AesBodyAttribute>() != null;
if (!hasAesBodyAttr) continue; if (!hasAesBodyAttr) continue;
// 1. 移除默认的 Query 参数描述(如果存在) // 1. 移除默认的 Query 参数描述(如果存在)
var queryParam = operation.Parameters var queryParam = operation.Parameters!
.FirstOrDefault(p => p.Name == parameter.Name); .FirstOrDefault(p => p.Name == parameter.Name);
if (queryParam != null) if (queryParam != null)
{ {
operation.Parameters.Remove(queryParam); operation.Parameters!.Remove(queryParam);
} }
// 2. 添加 Body 参数描述 // 2. 添加 Body 参数描述
@@ -1,27 +1,26 @@
using System; using System;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Text.Json.Nodes;
using Microsoft.OpenApi.Any;
namespace SimApi.SwaggerFilters; namespace SimApi.SwaggerFilters;
public class GlobalDynamicObjectSchemaFilter : ISchemaFilter public class GlobalDynamicObjectSchemaFilter : ISchemaFilter
{ {
public void Apply(OpenApiSchema schema, SchemaFilterContext context) public void Apply(IOpenApiSchema schema, SchemaFilterContext context)
{ {
if (!IsDynamicObjectType(context.Type)) return; if (!IsDynamicObjectType(context.Type)) return;
schema.AdditionalPropertiesAllowed = true; var oaSchema = schema as OpenApiSchema;
schema.AdditionalProperties = new OpenApiSchema oaSchema!.AdditionalPropertiesAllowed = true;
oaSchema.AdditionalProperties = new OpenApiSchema
{ {
Type = "object", // 表示 value 可以是任意类型(兼容所有类型) Type = JsonSchemaType.Object, // 表示 value 可以是任意类型(兼容所有类型)
Nullable = true
}; };
// 2. 覆盖默认示例,使用包含多种类型的示例 // 2. 覆盖默认示例,使用包含多种类型的示例
schema.Example = CreateMultiTypeExample(); oaSchema.Example = CreateMultiTypeExample();
} }
// 判断是否为需要处理的“动态对象”类型 // 判断是否为需要处理的“动态对象”类型
@@ -33,30 +32,39 @@ public class GlobalDynamicObjectSchemaFilter : ISchemaFilter
{ {
return true; return true;
} }
if (type == typeof(object)) if (type == typeof(object))
{ {
return true; return true;
} }
return type.Name.Contains("AnonymousType") && type.Namespace == null; return type.Name.Contains("AnonymousType") && type.Namespace == null;
} }
// 创建包含多种类型的示例(覆盖默认的 string 示例) // 创建包含多种类型的示例(覆盖默认的 string 示例)
private OpenApiObject CreateMultiTypeExample() private JsonNode CreateMultiTypeExample()
{ {
return new OpenApiObject return new JsonObject
{ {
["stringProp"] = new OpenApiString("example string"), // 字符串 // 字符串类型:JsonValue.Create 包装字符串
["numberProp"] = new OpenApiInteger(123), // 数字 ["stringProp"] = JsonValue.Create("example string"),
["boolProp"] = new OpenApiBoolean(true), // 布尔值 // 数字类型:支持 int/long/double 等,JsonValue 自动适配
["objectProp"] = new OpenApiObject // 嵌套对象 ["numberProp"] = JsonValue.Create(123),
// 布尔类型
["boolProp"] = JsonValue.Create(true),
// 嵌套对象:JsonObject 对应 OpenApiObject
["objectProp"] = new JsonObject
{ {
["nestedKey"] = new OpenApiString("nested value") ["nestedKey"] = JsonValue.Create("nested value")
}, },
["arrayProp"] = new OpenApiArray // 数组 // 数组类型:JsonArray 对应 OpenApiArray
["arrayProp"] = new JsonArray
{ {
new OpenApiInteger(1), JsonValue.Create(1), // 数组内数字
new OpenApiString("two") JsonValue.Create("two") // 数组内字符串
} },
// 可选:添加 null 值示例(若需要)
["nullProp"] = null
}; };
} }
} }
+25
View File
@@ -0,0 +1,25 @@
using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;
namespace SimApi.SwaggerFilters;
public class RemoveEmptyTagsFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
// 步骤1:收集所有有接口的 Tag 名称
var tagsWithOperations = swaggerDoc.Paths.Values
.SelectMany(path => path.Operations!.Values)
.SelectMany(op => op.Tags!.Select(t => t.Name))
.Distinct()
.ToList();
// 步骤2:移除无接口的空 Tag
var emptyTags = swaggerDoc.Tags!.Where(t => !tagsWithOperations.Contains(t.Name)).ToList();
foreach (var tag in emptyTags)
{
swaggerDoc.Tags!.Remove(tag);
}
}
}
+6 -22
View File
@@ -1,7 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using SimApi.Attributes; using SimApi.Attributes;
@@ -18,28 +18,12 @@ namespace SimApi.SwaggerFilters
|| ||
// 控制器上有 [SimApiAuth](继承到所有方法) // 控制器上有 [SimApiAuth](继承到所有方法)
context.MethodInfo.DeclaringType?.GetCustomAttributes<SimApiAuthAttribute>(true).Any() == true; context.MethodInfo.DeclaringType?.GetCustomAttributes<SimApiAuthAttribute>(true).Any() == true;
if (requiresAuth) if (!requiresAuth) return;
operation.Security ??= new List<OpenApiSecurityRequirement>();
operation.Security.Add(new OpenApiSecurityRequirement()
{ {
// 添加授权要求:关联步骤 2 中定义的 "SimApiAuth" 安全方案 { new OpenApiSecuritySchemeReference("SimApiAuth", context.Document), [] }
operation.Security = new List<OpenApiSecurityRequirement> });
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "SimApiAuth" // 必须与 AddSecurityDefinition 的第一个参数一致
}
},
[] // 无需指定作用域(scope)时留空
}
}
};
}
// 未标记 [SimApiAuth] 的接口:不添加安全要求,Swagger 不显示锁图标
} }
} }
} }
@@ -1,7 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -46,7 +46,7 @@ public class SimApiResponseOperationFilter : IOperationFilter
var schema = context.SchemaGenerator.GenerateSchema(wrappedType, context.SchemaRepository); var schema = context.SchemaGenerator.GenerateSchema(wrappedType, context.SchemaRepository);
// 5. 替换 Swagger 文档中的响应类型(只保留 200 OK 的响应,匹配过滤器逻辑) // 5. 替换 Swagger 文档中的响应类型(只保留 200 OK 的响应,匹配过滤器逻辑)
operation.Responses.Clear(); // 清除默认响应(如 200 返回原始类型) operation.Responses!.Clear(); // 清除默认响应(如 200 返回原始类型)
operation.Responses.Add("200", new OpenApiResponse operation.Responses.Add("200", new OpenApiResponse
{ {
Description = "请求成功", Description = "请求成功",
+10 -6
View File
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Models; using Microsoft.OpenApi;
using SimApi.Attributes; using SimApi.Attributes;
using SimApi.ModelBinders; using SimApi.ModelBinders;
using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.SwaggerGen;
@@ -67,20 +67,24 @@ public class SimApiSignOperationFilter(IServiceProvider serviceProvider) : IOper
signParameters.AddRange(keyProvider.SignFields.Where(x => x != keyProvider.AppIdName) signParameters.AddRange(keyProvider.SignFields.Where(x => x != keyProvider.AppIdName)
.Select(f => (f, string.Empty, true))); .Select(f => (f, string.Empty, true)));
operation.Parameters ??= new List<IOpenApiParameter>();
foreach (var (name, description, required) in signParameters) foreach (var (name, description, required) in signParameters)
{ {
if (operation.Parameters?.Any(p => p.Name == name) == true) if (operation.Parameters?.Any(p => p.Name == name) == true)
{ {
var tmp = operation.Parameters?.FirstOrDefault(p => p.Name == name); var tmp = operation.Parameters?.FirstOrDefault(p => p.Name == name);
if (tmp != null) if (tmp is OpenApiParameter concreteParam)
{ {
tmp.Required = required; // 重新赋值只读属性(通过实例化新对象覆盖,或直接修改具体类的可写属性)
tmp.Description = description; concreteParam.Required = required; // OpenApiParameter 的 Required 有 setter
concreteParam.Description = description;
} }
continue; continue;
} }
operation.Parameters ??= new List<OpenApiParameter>(); operation.Parameters ??= new List<IOpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter operation.Parameters.Add(new OpenApiParameter
{ {
Name = name, Name = name,
@@ -89,7 +93,7 @@ public class SimApiSignOperationFilter(IServiceProvider serviceProvider) : IOper
Required = required, Required = required,
Schema = new OpenApiSchema Schema = new OpenApiSchema
{ {
Type = "string" // 签名相关参数通常为字符串类型 Type = JsonSchemaType.String // 签名相关参数通常为字符串类型
} }
}); });
} }