feat(shared-web): add shared swagger and tracing utilities
This commit is contained in:
62
0_Document/10_TODO.md
Normal file
62
0_Document/10_TODO.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# TODO Roadmap
|
||||||
|
|
||||||
|
说明:本清单覆盖当前阶段的骨架搭建与核心基础能力(不含部署与CI/CD,留到项目跑通后再做)。
|
||||||
|
|
||||||
|
## A. 基础骨架与规范
|
||||||
|
- [x] 统一返回结果/异常处理中间件(Shared.Web)
|
||||||
|
- [x] 模型验证、验证失败统一输出(Shared.Web)
|
||||||
|
- [x] 统一日志(Serilog)与请求日志/TraceId(Shared.Web)
|
||||||
|
- [x] API 版本化与分组(AdminApi、MiniApi、UserApi)
|
||||||
|
- [x] Swagger 定制(鉴权按钮、分组说明、示例)
|
||||||
|
- [x] 安全中间件:Security Headers、CORS 策略(按端区分)
|
||||||
|
|
||||||
|
## B. 认证与权限
|
||||||
|
- [ ] JWT 颁发与刷新(AdminApi、MiniApi)
|
||||||
|
- [ ] RBAC 权限模型(角色/权限/策略)与特性授权(AdminApi)
|
||||||
|
- [ ] 小程序登录(微信 code2Session)并绑定用户账户(MiniApi)
|
||||||
|
- [ ] 登录防刷限流(MiniApi)
|
||||||
|
|
||||||
|
## C. 多租户与参数字典
|
||||||
|
- [ ] 多租户中间件:从 Header/域名解析租户(Shared.Web + Tenancy)
|
||||||
|
- [ ] EF Core 全局查询过滤(tenant_id)
|
||||||
|
- [ ] 参数字典模块(系统参数/业务参数)CRUD 与缓存(Dictionary 模块)
|
||||||
|
|
||||||
|
## D. 数据访问与多数据源
|
||||||
|
- [ ] EF Core 10 基础上下文、实体基类、审计字段
|
||||||
|
- [ ] 读写分离/多数据源配置(主写、从读;或按租户切库预留)
|
||||||
|
- [ ] Dapper 基础设施封装(统计/报表类查询)
|
||||||
|
|
||||||
|
## E. 文件与存储
|
||||||
|
- [ ] 存储模块抽象(本地/MinIO/云厂商适配)
|
||||||
|
- [ ] 上传接口(AdminApi、MiniApi)与签名直传预留
|
||||||
|
- [ ] 图片/文件访问安全策略(防盗链、过期签名)
|
||||||
|
|
||||||
|
## F. 短信与消息队列
|
||||||
|
- [ ] 短信模块(阿里云/腾讯云 适配占位)与验证码发送
|
||||||
|
- [ ] MQ 模块(RabbitMQ)Publisher/Subscriber 抽象
|
||||||
|
- [ ] 业务事件定义(订单创建/支付成功等)与事件发布入口
|
||||||
|
|
||||||
|
## G. 调度与定时任务
|
||||||
|
- [ ] 调度模块(Quartz/Hangfire 二选一,默认 Hangfire)
|
||||||
|
- [ ] 基础任务:订单超时取消、优惠券过期处理、日志清理
|
||||||
|
- [ ] 调度面板(后续 AdminUI 对接)
|
||||||
|
|
||||||
|
## H. 第三方配送对接(仅第三方)
|
||||||
|
- [ ] 配送适配抽象(达达/闪送/顺丰同城等)
|
||||||
|
- [ ] 统一下单/取消/查询接口与回调验签
|
||||||
|
- [ ] AdminApi 后台运力单查询与补单
|
||||||
|
|
||||||
|
## I. 网关与横切能力
|
||||||
|
- [ ] YARP 路由拆分(/api/admin、/api/mini、/api/user)
|
||||||
|
- [ ] 网关级限流与请求日志
|
||||||
|
- [ ] 透传鉴权/租户标识与统一错误页
|
||||||
|
|
||||||
|
## J. 测试与质量
|
||||||
|
- [ ] 单元测试工程骨架(xUnit + FluentAssertions)
|
||||||
|
- [ ] 集成测试基座(WebApplicationFactory、测试容器)
|
||||||
|
- [ ] 静态分析与风格规范(.editorconfig)
|
||||||
|
|
||||||
|
## K. 文档与规范落地
|
||||||
|
- [ ] 在文档中补充:仅第三方配送的接口与回调规范
|
||||||
|
- [ ] MiniApi 认证流程图(微信登录)与错误码
|
||||||
|
- [ ] 模块间调用关系图与依赖边界
|
||||||
28
src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs
Normal file
28
src/Api/TakeoutSaaS.AdminApi/Controllers/HealthController.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.AdminApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 管理后台 - 健康检查。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[Route("api/admin/v{version:apiVersion}/[controller]")]
|
||||||
|
public class HealthController : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取服务健康状态。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>健康状态</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public IActionResult Get()
|
||||||
|
{
|
||||||
|
var payload = new { status = "OK", service = "AdminApi", time = DateTime.UtcNow };
|
||||||
|
return Ok(ApiResponse<object>.Ok(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/Api/TakeoutSaaS.AdminApi/Program.cs
Normal file
76
src/Api/TakeoutSaaS.AdminApi/Program.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Serilog;
|
||||||
|
using TakeoutSaaS.Module.Tenancy;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
using TakeoutSaaS.Shared.Web.Extensions;
|
||||||
|
using TakeoutSaaS.Shared.Web.Swagger;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Host.UseSerilog((context, _, configuration) =>
|
||||||
|
{
|
||||||
|
configuration
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.Enrich.WithProperty("Service", "AdminApi")
|
||||||
|
.WriteTo.Console();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSharedWebCore();
|
||||||
|
builder.Services.AddSharedSwagger(options =>
|
||||||
|
{
|
||||||
|
options.Title = "外卖SaaS - 管理后台";
|
||||||
|
options.Description = "管理后台 API 文档";
|
||||||
|
options.EnableAuthorization = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
var adminOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Admin");
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AdminApiCors", policy =>
|
||||||
|
{
|
||||||
|
ConfigureCorsPolicy(policy, adminOrigins);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseCors("AdminApiCors");
|
||||||
|
app.UseSharedWebCore();
|
||||||
|
app.UseSharedSwagger();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey)
|
||||||
|
{
|
||||||
|
var origins = configuration.GetSection(sectionKey).Get<string[]>();
|
||||||
|
return origins?
|
||||||
|
.Where(origin => !string.IsNullOrWhiteSpace(origin))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray() ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins)
|
||||||
|
{
|
||||||
|
if (origins.Length == 0)
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
policy.WithOrigins(origins)
|
||||||
|
.AllowCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
policy
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
}
|
||||||
28
src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs
Normal file
28
src/Api/TakeoutSaaS.MiniApi/Controllers/HealthController.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.MiniApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 小程序端 - 健康检查。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[Route("api/mini/v{version:apiVersion}/[controller]")]
|
||||||
|
public class HealthController : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取服务健康状态。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>健康状态</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public IActionResult Get()
|
||||||
|
{
|
||||||
|
var payload = new { status = "OK", service = "MiniApi", time = DateTime.UtcNow };
|
||||||
|
return Ok(ApiResponse<object>.Ok(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/Api/TakeoutSaaS.MiniApi/Program.cs
Normal file
76
src/Api/TakeoutSaaS.MiniApi/Program.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Serilog;
|
||||||
|
using TakeoutSaaS.Module.Tenancy;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
using TakeoutSaaS.Shared.Web.Extensions;
|
||||||
|
using TakeoutSaaS.Shared.Web.Swagger;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Host.UseSerilog((context, _, configuration) =>
|
||||||
|
{
|
||||||
|
configuration
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.Enrich.WithProperty("Service", "MiniApi")
|
||||||
|
.WriteTo.Console();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSharedWebCore();
|
||||||
|
builder.Services.AddSharedSwagger(options =>
|
||||||
|
{
|
||||||
|
options.Title = "外卖SaaS - 小程序端";
|
||||||
|
options.Description = "小程序 API 文档";
|
||||||
|
options.EnableAuthorization = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
var miniOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:Mini");
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("MiniApiCors", policy =>
|
||||||
|
{
|
||||||
|
ConfigureCorsPolicy(policy, miniOrigins);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseCors("MiniApiCors");
|
||||||
|
app.UseSharedWebCore();
|
||||||
|
app.UseSharedSwagger();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey)
|
||||||
|
{
|
||||||
|
var origins = configuration.GetSection(sectionKey).Get<string[]>();
|
||||||
|
return origins?
|
||||||
|
.Where(origin => !string.IsNullOrWhiteSpace(origin))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray() ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins)
|
||||||
|
{
|
||||||
|
if (origins.Length == 0)
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
policy.WithOrigins(origins)
|
||||||
|
.AllowCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
policy
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
}
|
||||||
28
src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs
Normal file
28
src/Api/TakeoutSaaS.UserApi/Controllers/HealthController.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
using TakeoutSaaS.Shared.Web.Api;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.UserApi.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户端 - 健康检查。
|
||||||
|
/// </summary>
|
||||||
|
[ApiVersion("1.0")]
|
||||||
|
[AllowAnonymous]
|
||||||
|
[Route("api/user/v{version:apiVersion}/[controller]")]
|
||||||
|
public class HealthController : BaseApiController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取服务健康状态。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>健康状态</returns>
|
||||||
|
[HttpGet]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status200OK)]
|
||||||
|
public IActionResult Get()
|
||||||
|
{
|
||||||
|
var payload = new { status = "OK", service = "UserApi", time = DateTime.UtcNow };
|
||||||
|
return Ok(ApiResponse<object>.Ok(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
76
src/Api/TakeoutSaaS.UserApi/Program.cs
Normal file
76
src/Api/TakeoutSaaS.UserApi/Program.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Cors.Infrastructure;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Serilog;
|
||||||
|
using TakeoutSaaS.Module.Tenancy;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||||
|
using TakeoutSaaS.Shared.Web.Extensions;
|
||||||
|
using TakeoutSaaS.Shared.Web.Swagger;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Host.UseSerilog((context, _, configuration) =>
|
||||||
|
{
|
||||||
|
configuration
|
||||||
|
.Enrich.FromLogContext()
|
||||||
|
.Enrich.WithProperty("Service", "UserApi")
|
||||||
|
.WriteTo.Console();
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSharedWebCore();
|
||||||
|
builder.Services.AddSharedSwagger(options =>
|
||||||
|
{
|
||||||
|
options.Title = "外卖SaaS - 用户端";
|
||||||
|
options.Description = "C 端用户 API 文档";
|
||||||
|
options.EnableAuthorization = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
var userOrigins = ResolveCorsOrigins(builder.Configuration, "Cors:User");
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("UserApiCors", policy =>
|
||||||
|
{
|
||||||
|
ConfigureCorsPolicy(policy, userOrigins);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddScoped<ITenantProvider, TenantProvider>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.UseCors("UserApiCors");
|
||||||
|
app.UseSharedWebCore();
|
||||||
|
app.UseSharedSwagger();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
app.Run();
|
||||||
|
|
||||||
|
static string[] ResolveCorsOrigins(IConfiguration configuration, string sectionKey)
|
||||||
|
{
|
||||||
|
var origins = configuration.GetSection(sectionKey).Get<string[]>();
|
||||||
|
return origins?
|
||||||
|
.Where(origin => !string.IsNullOrWhiteSpace(origin))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray() ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ConfigureCorsPolicy(CorsPolicyBuilder policy, string[] origins)
|
||||||
|
{
|
||||||
|
if (origins.Length == 0)
|
||||||
|
{
|
||||||
|
policy.AllowAnyOrigin();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
policy.WithOrigins(origins)
|
||||||
|
.AllowCredentials();
|
||||||
|
}
|
||||||
|
|
||||||
|
policy
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Abstractions.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 轻量级 TraceId 上下文,便于跨层访问当前请求的追踪标识。
|
||||||
|
/// </summary>
|
||||||
|
public static class TraceContext
|
||||||
|
{
|
||||||
|
private static readonly AsyncLocal<string?> TraceIdHolder = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前请求的 TraceId。
|
||||||
|
/// </summary>
|
||||||
|
public static string? TraceId
|
||||||
|
{
|
||||||
|
get => TraceIdHolder.Value;
|
||||||
|
set => TraceIdHolder.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清理 TraceId,避免 AsyncLocal 污染其它请求。
|
||||||
|
/// </summary>
|
||||||
|
public static void Clear() => TraceIdHolder.Value = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
namespace TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 非泛型便捷封装。
|
||||||
|
/// </summary>
|
||||||
|
public static class ApiResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 仅返回成功消息(无数据)。
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<object> Success(string? message = "操作成功")
|
||||||
|
=> ApiResponse<object>.Ok(message: message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 成功且携带数据。
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<object> Ok(object? data, string? message = "操作成功")
|
||||||
|
=> data is null ? ApiResponse<object>.Ok(message: message) : ApiResponse<object>.Ok(data, message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误返回。
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<object> Failure(int code, string message)
|
||||||
|
=> ApiResponse<object>.Error(code, message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误返回(附带详情)。
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<object> Error(int code, string message, object? errors = null)
|
||||||
|
=> ApiResponse<object>.Error(code, message, errors);
|
||||||
|
}
|
||||||
105
src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs
Normal file
105
src/Core/TakeoutSaaS.Shared.Abstractions/Results/ApiResponse.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Diagnostics;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一的 API 返回结果包装。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">数据载荷类型</typeparam>
|
||||||
|
public sealed record class ApiResponse<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 是否成功。
|
||||||
|
/// </summary>
|
||||||
|
public bool Success { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 状态/错误码(默认 200)。
|
||||||
|
/// </summary>
|
||||||
|
public int Code { get; init; } = 200;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提示信息。
|
||||||
|
/// </summary>
|
||||||
|
public string? Message { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 业务数据。
|
||||||
|
/// </summary>
|
||||||
|
public T? Data { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误详情(如字段验证错误)。
|
||||||
|
/// </summary>
|
||||||
|
public object? Errors { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// TraceId,便于链路追踪。
|
||||||
|
/// </summary>
|
||||||
|
public string TraceId { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 时间戳(UTC)。
|
||||||
|
/// </summary>
|
||||||
|
public DateTime Timestamp { get; init; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 成功返回。
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<T> Ok(T data, string? message = "操作成功")
|
||||||
|
=> Create(true, 200, message, data);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 无数据的成功返回。
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<T> Ok(string? message = "操作成功")
|
||||||
|
=> Create(true, 200, message, default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兼容旧名称:成功结果。
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<T> SuccessResult(T data, string? message = "操作成功")
|
||||||
|
=> Ok(data, message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 错误返回。
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<T> Error(int code, string message, object? errors = null)
|
||||||
|
=> Create(false, code, message, default, errors);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 兼容旧名称:失败结果。
|
||||||
|
/// </summary>
|
||||||
|
public static ApiResponse<T> Failure(int code, string message)
|
||||||
|
=> Error(code, message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 附加错误详情。
|
||||||
|
/// </summary>
|
||||||
|
public ApiResponse<T> WithErrors(object? errors)
|
||||||
|
=> this with { Errors = errors };
|
||||||
|
|
||||||
|
private static ApiResponse<T> Create(bool success, int code, string? message, T? data, object? errors = null)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Success = success,
|
||||||
|
Code = code,
|
||||||
|
Message = message,
|
||||||
|
Data = data,
|
||||||
|
Errors = errors,
|
||||||
|
TraceId = ResolveTraceId(),
|
||||||
|
Timestamp = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ResolveTraceId()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(TraceContext.TraceId))
|
||||||
|
{
|
||||||
|
return TraceContext.TraceId!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Activity.Current?.Id ?? Guid.NewGuid().ToString("N");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using TakeoutSaaS.Shared.Web.Middleware;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Web.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Web 应用中间件扩展。
|
||||||
|
/// </summary>
|
||||||
|
public static class ApplicationBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 按规范启用 TraceId、请求日志、异常映射与安全响应头。
|
||||||
|
/// </summary>
|
||||||
|
public static IApplicationBuilder UseSharedWebCore(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.UseMiddleware<CorrelationIdMiddleware>();
|
||||||
|
app.UseMiddleware<RequestLoggingMiddleware>();
|
||||||
|
app.UseMiddleware<ExceptionHandlingMiddleware>();
|
||||||
|
app.UseMiddleware<SecurityHeadersMiddleware>();
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using TakeoutSaaS.Shared.Web.Filters;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Web.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared.Web 服务注册扩展。
|
||||||
|
/// </summary>
|
||||||
|
public static class ServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 注册控制器、模型验证、API 版本化等基础能力。
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSharedWebCore(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddHttpContextAccessor();
|
||||||
|
services.AddEndpointsApiExplorer();
|
||||||
|
|
||||||
|
services
|
||||||
|
.AddControllers(options =>
|
||||||
|
{
|
||||||
|
options.Filters.Add<ValidateModelAttribute>();
|
||||||
|
})
|
||||||
|
.AddNewtonsoftJson();
|
||||||
|
|
||||||
|
services.Configure<ApiBehaviorOptions>(options =>
|
||||||
|
{
|
||||||
|
options.SuppressModelStateInvalidFilter = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddApiVersioning(options =>
|
||||||
|
{
|
||||||
|
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||||
|
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||||
|
options.ReportApiVersions = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddVersionedApiExplorer(setup =>
|
||||||
|
{
|
||||||
|
setup.GroupNameFormat = "'v'VVV";
|
||||||
|
setup.SubstituteApiVersionInUrl = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Web.Filters;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模型验证过滤器:将模型验证错误统一为ApiResponse输出
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ValidateModelAttribute : ActionFilterAttribute
|
||||||
|
{
|
||||||
|
public override void OnActionExecuting(ActionExecutingContext context)
|
||||||
|
{
|
||||||
|
if (!context.ModelState.IsValid)
|
||||||
|
{
|
||||||
|
var errors = context.ModelState
|
||||||
|
.Where(kv => kv.Value?.Errors.Count > 0)
|
||||||
|
.ToDictionary(
|
||||||
|
kv => kv.Key,
|
||||||
|
kv => kv.Value!.Errors.Select(e => string.IsNullOrWhiteSpace(e.ErrorMessage) ? "Invalid" : e.ErrorMessage).ToArray()
|
||||||
|
);
|
||||||
|
|
||||||
|
var response = ApiResponse<object>.Error(ErrorCodes.ValidationFailed, "一个或多个验证错误", errors);
|
||||||
|
context.Result = new UnprocessableEntityObjectResult(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Diagnostics;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 统一 TraceId/CorrelationId,贯穿日志与响应。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CorrelationIdMiddleware
|
||||||
|
{
|
||||||
|
private const string TraceHeader = "X-Trace-Id";
|
||||||
|
private const string RequestHeader = "X-Request-Id";
|
||||||
|
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<CorrelationIdMiddleware> _logger;
|
||||||
|
|
||||||
|
public CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
var traceId = ResolveTraceId(context);
|
||||||
|
context.TraceIdentifier = traceId;
|
||||||
|
TraceContext.TraceId = traceId;
|
||||||
|
|
||||||
|
context.Response.OnStarting(() =>
|
||||||
|
{
|
||||||
|
context.Response.Headers[TraceHeader] = traceId;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
});
|
||||||
|
|
||||||
|
using (_logger.BeginScope(new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["TraceId"] = traceId
|
||||||
|
}))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
TraceContext.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveTraceId(HttpContext context)
|
||||||
|
{
|
||||||
|
if (TryGetHeader(context, TraceHeader, out var traceId))
|
||||||
|
{
|
||||||
|
return traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetHeader(context, RequestHeader, out var requestId))
|
||||||
|
{
|
||||||
|
return requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Guid.NewGuid().ToString("N");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetHeader(HttpContext context, string headerName, out string value)
|
||||||
|
{
|
||||||
|
if (context.Request.Headers.TryGetValue(headerName, out var values))
|
||||||
|
{
|
||||||
|
var headerValue = values.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(headerValue))
|
||||||
|
{
|
||||||
|
value = headerValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Results;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 全局异常处理中间件,将异常统一映射为 ApiResponse。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ExceptionHandlingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
|
||||||
|
private readonly IHostEnvironment _environment;
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
};
|
||||||
|
|
||||||
|
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger, IHostEnvironment environment)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
_environment = environment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "未处理异常:{Message}", ex.Message);
|
||||||
|
await HandleExceptionAsync(context, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||||
|
{
|
||||||
|
var (statusCode, response) = BuildErrorResponse(exception);
|
||||||
|
|
||||||
|
if (_environment.IsDevelopment())
|
||||||
|
{
|
||||||
|
response = response with
|
||||||
|
{
|
||||||
|
Message = exception.Message,
|
||||||
|
Errors = new
|
||||||
|
{
|
||||||
|
response.Errors,
|
||||||
|
detail = exception.ToString()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Response.StatusCode = statusCode;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
return context.Response.WriteAsJsonAsync(response, SerializerOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int StatusCode, ApiResponse<object> Response) BuildErrorResponse(Exception exception)
|
||||||
|
{
|
||||||
|
return exception switch
|
||||||
|
{
|
||||||
|
ValidationException validationException => (
|
||||||
|
StatusCodes.Status422UnprocessableEntity,
|
||||||
|
ApiResponse<object>.Error(ErrorCodes.ValidationFailed, "请求参数验证失败", validationException.Errors)),
|
||||||
|
BusinessException businessException => (
|
||||||
|
StatusCodes.Status422UnprocessableEntity,
|
||||||
|
ApiResponse<object>.Error(businessException.ErrorCode, businessException.Message)),
|
||||||
|
_ => (
|
||||||
|
StatusCodes.Status500InternalServerError,
|
||||||
|
ApiResponse<object>.Error(ErrorCodes.InternalServerError, "服务器开小差啦,请稍后再试"))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Diagnostics;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基础请求日志(方法、路径、耗时、状态码、TraceId)。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RequestLoggingMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ILogger<RequestLoggingMiddleware> _logger;
|
||||||
|
|
||||||
|
public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
var traceId = TraceContext.TraceId ?? context.TraceIdentifier;
|
||||||
|
_logger.LogInformation(
|
||||||
|
"HTTP {Method} {Path} => {StatusCode} ({Elapsed} ms) TraceId:{TraceId}",
|
||||||
|
context.Request.Method,
|
||||||
|
context.Request.Path,
|
||||||
|
context.Response.StatusCode,
|
||||||
|
stopwatch.Elapsed.TotalMilliseconds,
|
||||||
|
traceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Web.Middleware;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 安全响应头中间件
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SecurityHeadersMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public SecurityHeadersMiddleware(RequestDelegate next)
|
||||||
|
{
|
||||||
|
_next = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context)
|
||||||
|
{
|
||||||
|
var headers = context.Response.Headers;
|
||||||
|
headers["X-Content-Type-Options"] = "nosniff";
|
||||||
|
headers["X-Frame-Options"] = "DENY";
|
||||||
|
headers["X-XSS-Protection"] = "1; mode=block";
|
||||||
|
headers["Referrer-Policy"] = "no-referrer";
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Web.Swagger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据 API 版本动态注册 Swagger 文档。
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
|
||||||
|
{
|
||||||
|
private readonly IApiVersionDescriptionProvider _provider;
|
||||||
|
private readonly SwaggerDocumentSettings _settings;
|
||||||
|
|
||||||
|
public ConfigureSwaggerOptions(
|
||||||
|
IApiVersionDescriptionProvider provider,
|
||||||
|
IOptions<SwaggerDocumentSettings> settings)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
_settings = settings.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(SwaggerGenOptions options)
|
||||||
|
{
|
||||||
|
foreach (var description in _provider.ApiVersionDescriptions)
|
||||||
|
{
|
||||||
|
var info = new OpenApiInfo
|
||||||
|
{
|
||||||
|
Title = $"{_settings.Title} {description.ApiVersion}",
|
||||||
|
Version = description.ApiVersion.ToString(),
|
||||||
|
Description = description.IsDeprecated
|
||||||
|
? $"{_settings.Description}(该版本已弃用)"
|
||||||
|
: _settings.Description
|
||||||
|
};
|
||||||
|
|
||||||
|
options.SwaggerGeneratorOptions.SwaggerDocs[description.GroupName] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_settings.EnableAuthorization)
|
||||||
|
{
|
||||||
|
var scheme = new OpenApiSecurityScheme
|
||||||
|
{
|
||||||
|
Name = "Authorization",
|
||||||
|
Description = "在下方输入 Bearer Token,格式:Bearer {token}",
|
||||||
|
In = ParameterLocation.Header,
|
||||||
|
Type = SecuritySchemeType.Http,
|
||||||
|
Scheme = "bearer",
|
||||||
|
BearerFormat = "JWT"
|
||||||
|
};
|
||||||
|
|
||||||
|
options.SwaggerGeneratorOptions.SecuritySchemes["Bearer"] = scheme;
|
||||||
|
options.SwaggerGeneratorOptions.SecurityRequirements.Add(new OpenApiSecurityRequirement
|
||||||
|
{
|
||||||
|
{ scheme, Array.Empty<string>() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace TakeoutSaaS.Shared.Web.Swagger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swagger 文档配置。
|
||||||
|
/// </summary>
|
||||||
|
public class SwaggerDocumentSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文档标题。
|
||||||
|
/// </summary>
|
||||||
|
public string Title { get; set; } = "TakeoutSaaS API";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 描述信息。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用 JWT Authorize 按钮。
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAuthorization { get; set; } = true;
|
||||||
|
}
|
||||||
65
src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs
Normal file
65
src/Core/TakeoutSaaS.Shared.Web/Swagger/SwaggerExtensions.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ApiExplorer;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Shared.Web.Swagger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swagger 注册/启用扩展。
|
||||||
|
/// </summary>
|
||||||
|
public static class SwaggerExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 注入统一的 Swagger 服务。
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddSharedSwagger(this IServiceCollection services, Action<SwaggerDocumentSettings>? configure = null)
|
||||||
|
{
|
||||||
|
services.AddSwaggerGen();
|
||||||
|
services.AddSingleton(provider =>
|
||||||
|
{
|
||||||
|
var settings = new SwaggerDocumentSettings();
|
||||||
|
configure?.Invoke(settings);
|
||||||
|
return settings;
|
||||||
|
});
|
||||||
|
services.AddSingleton<IConfigureOptions<SwaggerGenOptions>>(provider =>
|
||||||
|
new ConfigureSwaggerOptions(
|
||||||
|
provider.GetRequiredService<IApiVersionDescriptionProvider>(),
|
||||||
|
Options.Create(provider.GetRequiredService<SwaggerDocumentSettings>())));
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 开发环境启用 Swagger UI(自动注册所有版本)。
|
||||||
|
/// </summary>
|
||||||
|
public static IApplicationBuilder UseSharedSwagger(this IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
var env = app.ApplicationServices.GetRequiredService<IHostEnvironment>();
|
||||||
|
if (!env.IsDevelopment())
|
||||||
|
{
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider = app.ApplicationServices.GetRequiredService<IApiVersionDescriptionProvider>();
|
||||||
|
var settings = app.ApplicationServices.GetRequiredService<SwaggerDocumentSettings>();
|
||||||
|
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI(options =>
|
||||||
|
{
|
||||||
|
foreach (var description in provider.ApiVersionDescriptions)
|
||||||
|
{
|
||||||
|
options.SwaggerEndpoint(
|
||||||
|
$"/swagger/{description.GroupName}/swagger.json",
|
||||||
|
$"{settings.Title} {description.ApiVersion}");
|
||||||
|
}
|
||||||
|
|
||||||
|
options.DisplayRequestDuration();
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="5.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="5.1.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.6.2" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="6.6.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user