Skip to content

Commit d8a32f3

Browse files
committed
文档相关修复
1 parent e17b9d9 commit d8a32f3

9 files changed

Lines changed: 347 additions & 64 deletions

Attributes/SimApiDocAttribute.cs

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,105 @@
1-
using System;
1+
using System;
22
using Swashbuckle.AspNetCore.Annotations;
33

44
namespace SimApi.Attributes;
55

66
/// <summary>
7-
/// 快捷自定义接口文档类
7+
/// 快捷自定义接口文档类(所有参数均可省略, 支持命名参数)。
8+
/// 对应仓颉版 SimApiDoc:
9+
/// tags → 接口标签(逗号分隔, 如 "认证,用户")
10+
/// name → API 名称(映射到 Summary, 作为文档接口标题)
11+
/// description → API 详细描述
12+
/// groupNames → 所属文档组(逗号分隔, 如 "api,admin"; "*" 表示所有文档; null/空 → 仅默认 "api" 文档)
13+
/// ignore → true 时不出现在任何文档中(路由不受影响)
14+
/// 写法示例:
15+
/// [SimApiDoc("认证", "登录")] 位置参数(保持旧版兼容)
16+
/// [SimApiDoc(tags: "认证", name: "登录", description: "...")]
17+
/// [SimApiDoc(groupNames: "api,admin")] 仅指定文档组
18+
/// [SimApiDoc(GroupNames = "*", Ignore = false)] 属性命名参数
19+
/// [SimApiDoc] 全部默认(可标在类上, 仅用 Ignore/GroupNames 等)
20+
/// 属性命名参数优先于构造命名参数。
821
/// </summary>
922
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
1023
public class SimApiDocAttribute : SwaggerOperationAttribute
1124
{
1225
/// <summary>
13-
/// 定义接口说明
26+
/// 出现在所有文档组中的通配符
1427
/// </summary>
15-
/// <param name="tags">接口分组列表</param>
28+
public const string AllGroups = "*";
29+
30+
/// <summary>
31+
/// 接口所属的文档组(逗号分隔, 如 "api,admin"); "*" 表示所有文档; null/空 表示未分组(仅默认 "api" 文档)
32+
/// </summary>
33+
public string? GroupNames { get; set; }
34+
35+
/// <summary>
36+
/// 为 true 时该接口不出现在任何文档中(不影响路由)
37+
/// </summary>
38+
public bool Ignore { get; set; }
39+
40+
/// <summary>
41+
/// 定义接口说明(全部可选)
42+
/// </summary>
43+
/// <param name="tags">接口标签, 逗号分隔, 如 "认证,用户"</param>
1644
/// <param name="name">接口名称</param>
1745
/// <param name="description">接口描述</param>
18-
public SimApiDocAttribute(string[] tags, string name, string? description = null)
46+
/// <param name="groupNames">所属文档组, 逗号分隔; "*" 表示所有文档</param>
47+
/// <param name="ignore">true 时不出现在任何文档</param>
48+
public SimApiDocAttribute(string? tags = null, string? name = null, string? description = null,
49+
string? groupNames = null, bool ignore = false)
1950
{
20-
Tags = tags;
21-
Summary = name;
22-
if (description != null)
23-
{
24-
Description = description;
25-
}
26-
// Consumes = new[] {"application/json"};
27-
// Produces = new[] {"application/json"};
51+
Apply(tags, name, description, groupNames, ignore);
2852
}
2953

3054
/// <summary>
31-
/// 定义接口说明
55+
/// 定义接口说明(标签以数组传入)
3256
/// </summary>
33-
/// <param name="tag">接口分组</param>
57+
/// <param name="tags">接口标签列表</param>
3458
/// <param name="name">接口名称</param>
3559
/// <param name="description">接口描述</param>
36-
public SimApiDocAttribute(string tag, string name, string? description = null) : this([tag], name, description)
60+
/// <param name="groupNames">所属文档组, 逗号分隔; "*" 表示所有文档</param>
61+
/// <param name="ignore">true 时不出现在任何文档</param>
62+
public SimApiDocAttribute(string[] tags, string? name = null, string? description = null,
63+
string? groupNames = null, bool ignore = false)
64+
{
65+
Apply(tags, name, description, groupNames, ignore);
66+
}
67+
68+
private void Apply(string? tags, string? name, string? description,
69+
string? groupNames, bool ignore)
70+
{
71+
if (!string.IsNullOrWhiteSpace(tags))
72+
{
73+
Tags = tags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
74+
}
75+
76+
ApplyCore(name, description, groupNames, ignore);
77+
}
78+
79+
private void Apply(string[] tags, string? name, string? description,
80+
string? groupNames, bool ignore)
3781
{
82+
if (tags is { Length: > 0 })
83+
{
84+
Tags = tags;
85+
}
86+
87+
ApplyCore(name, description, groupNames, ignore);
88+
}
89+
90+
private void ApplyCore(string? name, string? description, string? groupNames, bool ignore)
91+
{
92+
if (!string.IsNullOrEmpty(name))
93+
{
94+
Summary = name;
95+
}
96+
97+
if (!string.IsNullOrEmpty(description))
98+
{
99+
Description = description;
100+
}
101+
102+
GroupNames = groupNames;
103+
Ignore = ignore;
38104
}
39-
}
105+
}

Configurations/SimApiDocOptions.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace SimApi.Configurations;
66
/// <summary>
77
/// 文档组配置
88
/// </summary>
9-
public class SimApiDocGroupOption(string id, string name, string description = "")
9+
public class SimApiDocGroupOption(string id, string name, string description = "", bool isDefault = false)
1010
{
1111
/// <summary>
1212
/// 文档标识
@@ -22,6 +22,11 @@ public class SimApiDocGroupOption(string id, string name, string description = "
2222
/// 文档描述
2323
/// </summary>
2424
public string Description { get; set; } = description!;
25+
26+
/// <summary>
27+
/// 是否为默认文档组: 未标注分组的接口全部归入此文档组
28+
/// </summary>
29+
public bool IsDefault { get; set; } = isDefault;
2530
}
2631

2732
/// <summary>
@@ -50,14 +55,20 @@ public class SimApiDocOptions
5055
/// </summary>
5156
public SimApiDocGroupOption[] ApiGroups { get; set; } =
5257
[
53-
new("api", "Api", "Api接口文档")
58+
new("api", "Api", "Api接口文档", isDefault: true)
5459
];
5560

5661
/// <summary>
5762
/// 授权配置
5863
/// </summary>
5964
public SimApiAuthOption ApiAuth { get; set; } = new();
6065

66+
/// <summary>
67+
/// 接口文档页面访问前缀, 默认 "docs"。
68+
/// 访问 /docs 即打开文档页面, JSON 地址为 /docs/{文档组Id}.json
69+
/// </summary>
70+
public string UrlPrefix { get; set; } = "docs";
71+
6172
/// <summary>
6273
/// 文档页面标题
6374
/// </summary>

Controllers/SimApiAuthController.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class SimApiAuthController(SimApiAuth auth) : SimApiBaseController
1111
/// 退出登陆
1212
/// </summary>
1313
/// <returns></returns>
14-
[HttpPost, SimApiDoc("认证", "退出登陆")]
14+
[HttpPost, SimApiDoc("认证", "退出登陆",groupNames: "*")]
1515
public void Logout()
1616
{
1717
if (Request.Headers.TryGetValue("Token", out var value))

Controllers/SimApiBuiltInRoutes.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using SimApi.Configurations;
4+
5+
namespace SimApi.Controllers;
6+
7+
/// <summary>
8+
/// SimApi 内置端点路由表中的一项。
9+
/// </summary>
10+
public sealed record SimApiBuiltInRoute(
11+
string RouteName,
12+
string Controller,
13+
string Action,
14+
string[] HttpMethods,
15+
string Path);
16+
17+
/// <summary>
18+
/// SimApi 内置端点路由表 —— 路径的唯一配置处。
19+
/// UseSimApi 用它注册约定路由(动态路由),
20+
/// SimApiBuiltInRoutesDescriptionProvider 用它生成文档条目。
21+
/// 某项路径为 null / 空时该端点既不注册路由、也不出现在文档中。
22+
/// </summary>
23+
public static class SimApiBuiltInRoutes
24+
{
25+
public static IEnumerable<SimApiBuiltInRoute> Get(SimApiRouteOptions options)
26+
{
27+
ArgumentNullException.ThrowIfNull(options);
28+
29+
if (!string.IsNullOrEmpty(options.UserInfoRoute))
30+
{
31+
yield return new SimApiBuiltInRoute(
32+
"UserInfo", "SimApiCommon", "UserInfo", ["POST"], options.UserInfoRoute);
33+
}
34+
35+
if (!string.IsNullOrEmpty(options.LogoutRoute))
36+
{
37+
yield return new SimApiBuiltInRoute(
38+
"Logout", "SimApiAuth", "Logout", ["POST"], options.LogoutRoute);
39+
}
40+
41+
if (!string.IsNullOrEmpty(options.WebConfigRoute))
42+
{
43+
// 控制器动作同时标注了 HttpGet/HttpPost
44+
yield return new SimApiBuiltInRoute(
45+
"WebConfig", "SimApiCommon", "WebConfig", ["GET", "POST"], options.WebConfigRoute);
46+
}
47+
}
48+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
using Microsoft.AspNetCore.Mvc.ApiExplorer;
5+
using Microsoft.AspNetCore.Mvc.Controllers;
6+
using Microsoft.AspNetCore.Mvc.ModelBinding;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using SimApi.Configurations;
9+
10+
namespace SimApi.Controllers;
11+
12+
/// <summary>
13+
/// 将 SimApiRouteOptions 动态配置的内置约定路由(UserInfo / Logout / WebConfig)
14+
/// 翻译成 ApiDescription, 使它们能像属性路由接口一样出现在 Swagger 文档中:
15+
/// 请求/响应 schema 由 Swashbuckle 自动分析, 方法上已有的 [SimApiDoc] 注解自动生效。
16+
/// 路径为 null / 空 的端点在此一并跳过(与路由注册保持一致)。
17+
/// </summary>
18+
public class SimApiBuiltInRoutesDescriptionProvider(
19+
IServiceProvider services,
20+
SimApiOptions options) : IApiDescriptionProvider
21+
{
22+
/// <summary>
23+
/// 默认 provider(DefaultApiDescriptionProvider)的 Order 为 -1000, 位于其后执行。
24+
/// </summary>
25+
public int Order => -900;
26+
27+
public void OnProvidersExecuting(ApiDescriptionProviderContext context)
28+
{
29+
}
30+
31+
public void OnProvidersExecuted(ApiDescriptionProviderContext context)
32+
{
33+
var modelMetadata = services.GetService<IModelMetadataProvider>();
34+
35+
foreach (var route in SimApiBuiltInRoutes.Get(options.SimApiRouteOptions))
36+
{
37+
var action = context.Actions.OfType<ControllerActionDescriptor>()
38+
.FirstOrDefault(a =>
39+
string.Equals(a.ControllerName, route.Controller, StringComparison.Ordinal) &&
40+
string.Equals(a.ActionName, route.Action, StringComparison.Ordinal));
41+
if (action is null)
42+
{
43+
continue;
44+
}
45+
46+
// 若该动作已被属性路由 / 其他 provider 收录, 跳过避免重复
47+
if (context.Results.Any(d => ReferenceEquals(d.ActionDescriptor, action)))
48+
{
49+
continue;
50+
}
51+
52+
foreach (var httpMethod in route.HttpMethods)
53+
{
54+
context.Results.Add(CreateDescription(action, route, httpMethod, modelMetadata));
55+
}
56+
}
57+
}
58+
59+
private static ApiDescription CreateDescription(
60+
ControllerActionDescriptor action,
61+
SimApiBuiltInRoute route,
62+
string httpMethod,
63+
IModelMetadataProvider? modelMetadata)
64+
{
65+
var apiDescription = new ApiDescription
66+
{
67+
ActionDescriptor = action,
68+
HttpMethod = httpMethod,
69+
RelativePath = route.Path.TrimStart('/')
70+
};
71+
72+
// 输入: 按真实方法参数翻译(内置端点均为零参, 此处保证将来加参也可分析)
73+
foreach (var parameter in action.Parameters)
74+
{
75+
var parameterDescription = new ApiParameterDescription
76+
{
77+
Name = parameter.Name,
78+
Type = parameter.ParameterType,
79+
ParameterDescriptor = parameter,
80+
Source = parameter.BindingInfo?.BindingSource ?? BindingSource.Body,
81+
IsRequired = true
82+
};
83+
if (modelMetadata is not null)
84+
{
85+
parameterDescription.ModelMetadata = modelMetadata.GetMetadataForType(parameter.ParameterType);
86+
}
87+
88+
apiDescription.ParameterDescriptions.Add(parameterDescription);
89+
}
90+
91+
// 输出: 方法返回类型 => 200 + json
92+
var returnType = Unwrap(action.MethodInfo.ReturnType);
93+
if (returnType != typeof(void))
94+
{
95+
var responseType = new ApiResponseType
96+
{
97+
StatusCode = 200,
98+
Type = returnType,
99+
IsDefaultResponse = true
100+
};
101+
if (modelMetadata is not null)
102+
{
103+
responseType.ModelMetadata = modelMetadata.GetMetadataForType(returnType);
104+
}
105+
106+
responseType.ApiResponseFormats.Add(new ApiResponseFormat { MediaType = "application/json" });
107+
apiDescription.SupportedResponseTypes.Add(responseType);
108+
}
109+
110+
return apiDescription;
111+
}
112+
113+
private static Type Unwrap(Type type) =>
114+
type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>)
115+
? type.GetGenericArguments()[0]
116+
: type;
117+
}

Controllers/SimApiCommonController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class SimApiCommonController(SimApiOptions simApiOptions) : SimApiBaseCon
1818
/// <param name="code">错误代码</param>
1919
/// <returns></returns>
2020
[HttpGet("exception/{code:int}")]
21-
[ApiExplorerSettings(IgnoreApi = true)]
21+
[SimApiDoc(ignore: true)]
2222
public void ExceptionHandler(int code)
2323
{
2424
Error(code);
@@ -29,6 +29,7 @@ public void ExceptionHandler(int code)
2929
/// </summary>
3030
/// <returns></returns>
3131
[HttpPost, HttpGet]
32+
[HttpPost, SimApiDoc("公共", "获取自定义配置",groupNames: "*")]
3233
public Dictionary<string, object> WebConfig()
3334
{
3435
var resp = simApiOptions.WebConfig!.ToDictionary();
@@ -49,6 +50,6 @@ public Dictionary<string, object> WebConfig()
4950
/// 获取已登录用户信息
5051
/// </summary>
5152
/// <returns></returns>
52-
[HttpPost, SimApiAuth, SimApiDoc("认证", "获取已登录用户信息")]
53+
[HttpPost, SimApiAuth, SimApiDoc("认证", "获取已登录用户信息",groupNames: "*")]
5354
public SimApiLoginItem UserInfo() => LoginInfo;
5455
}

Helpers/SimApiUtil.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public static class SimApiUtil
2828
{
2929
// ReferenceHandler = ReferenceHandler.Preserve,
3030
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All),
31-
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
31+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
3232
// DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
3333
};
3434

0 commit comments

Comments
 (0)