From 66317bad70715ced173a8546d51d7b9a74cc2cf1 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 13:08:49 +0800 Subject: [PATCH 1/5] =?UTF-8?q?ci:=20=E8=B0=83=E6=95=B4=E8=A7=A6=E5=8F=91?= =?UTF-8?q?=E5=88=86=E6=94=AF=E4=B8=BA=20main/dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 138c139..d0738c9 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -3,7 +3,11 @@ name: TakeoutSaaS CI/CD on: push: branches: - - master + - main + - dev + pull_request: + branches: + - main workflow_dispatch: env: From a1c57c481b7e2ff649e7173f3a25ad178b37247b Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 13:23:29 +0800 Subject: [PATCH 2/5] =?UTF-8?q?ci:=20=E8=B0=83=E6=95=B4=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E5=9C=B0=E5=9D=80=E5=8C=85=E5=90=AB=E5=91=BD=E5=90=8D=E7=A9=BA?= =?UTF-8?q?=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index d0738c9..6e12b41 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -17,6 +17,7 @@ env: DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_USER: ${{ secrets.DEPLOY_USER }} DEPLOY_PASSWORD: ${{ secrets.DEPLOY_PASSWORD }} + REGISTRY_NAMESPACE: kjkj-saas jobs: detect: @@ -106,15 +107,15 @@ jobs: case "$SERVICE" in admin-api) DOCKERFILE="src/Api/TakeoutSaaS.AdminApi/Dockerfile" - IMAGE="$REGISTRY/admin-api:$IMAGE_TAG" + IMAGE="$REGISTRY/$REGISTRY_NAMESPACE/admin-api:$IMAGE_TAG" ;; mini-api) DOCKERFILE="src/Api/TakeoutSaaS.MiniApi/Dockerfile" - IMAGE="$REGISTRY/mini-api:$IMAGE_TAG" + IMAGE="$REGISTRY/$REGISTRY_NAMESPACE/mini-api:$IMAGE_TAG" ;; user-api) DOCKERFILE="src/Api/TakeoutSaaS.UserApi/Dockerfile" - IMAGE="$REGISTRY/user-api:$IMAGE_TAG" + IMAGE="$REGISTRY/$REGISTRY_NAMESPACE/user-api:$IMAGE_TAG" ;; *) echo "未知服务:$SERVICE" @@ -152,15 +153,15 @@ jobs: case "$SERVICE" in admin-api) - IMAGE="$REGISTRY/admin-api:$IMAGE_TAG" + IMAGE="$REGISTRY/$REGISTRY_NAMESPACE/admin-api:$IMAGE_TAG" PORT=7801 ;; mini-api) - IMAGE="$REGISTRY/mini-api:$IMAGE_TAG" + IMAGE="$REGISTRY/$REGISTRY_NAMESPACE/mini-api:$IMAGE_TAG" PORT=7701 ;; user-api) - IMAGE="$REGISTRY/user-api:$IMAGE_TAG" + IMAGE="$REGISTRY/$REGISTRY_NAMESPACE/user-api:$IMAGE_TAG" PORT=7901 ;; *) From ec55857c68b59b413a70bfeadc6d587abbdd37f6 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 13:48:32 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=E6=B3=A8=E5=86=8C=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E9=9B=AA=E8=8A=B1Id=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Api/TakeoutSaaS.MiniApi/Program.cs | 3 +++ .../TakeoutSaaS.MiniApi.csproj | Bin 4212 -> 4490 bytes src/Api/TakeoutSaaS.UserApi/Program.cs | 3 +++ .../TakeoutSaaS.UserApi.csproj | Bin 3164 -> 3434 bytes 4 files changed, 6 insertions(+) diff --git a/src/Api/TakeoutSaaS.MiniApi/Program.cs b/src/Api/TakeoutSaaS.MiniApi/Program.cs index 34d6a45..3c5adac 100644 --- a/src/Api/TakeoutSaaS.MiniApi/Program.cs +++ b/src/Api/TakeoutSaaS.MiniApi/Program.cs @@ -13,12 +13,15 @@ using TakeoutSaaS.Module.Messaging.Extensions; using TakeoutSaaS.Module.Sms.Extensions; using TakeoutSaaS.Module.Storage.Extensions; using TakeoutSaaS.Module.Tenancy.Extensions; +using TakeoutSaaS.Shared.Abstractions.Ids; +using TakeoutSaaS.Shared.Kernel.Ids; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; var builder = WebApplication.CreateBuilder(args); const string logTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [TraceId:{TraceId}] [SpanId:{SpanId}] [Service:{Service}] {SourceContext} {Message:lj}{NewLine}{Exception}"; +builder.Services.AddSingleton(_ => new SnowflakeIdGenerator()); builder.Host.UseSerilog((_, _, configuration) => { configuration diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj index 0b5e0ba555ac630fdc3cc0baf896a5ac9f9e1cab..3ee3e92c95e1eb69b410ec29832c45a51da842f9 100644 GIT binary patch delta 365 zcmeyO(4{;ffSH$pYofn5Bk#nSsz73IJR|R9X+}pN8O<08BsViQas!nxC;*`igZ{*Y zvOqz7E)eO+={R{CQy7%x!|VrT-C|Avvl1uIV(|jAVkbMW#=%(%lcm^kVWTG2{Ws n9Eb=vlE~zEK5>wKE_n%1PyvI9Apod4pCJpVKAE9}!Hxj{(X>;i delta 214 zcmeBD{-Q7;U}A{Z#04r7kHt+^V|19D!5A_55M#q+7bdmIIZQH>m6)9-pJ56C(tVRR zF)x^Wh$CV078Wlc9WyzGH4aKEOtxS%g0o++A;gccTL8uLCg*U30O@65_6^RY$u?Ya zVDp{0eI_5{s+#P?ePc2cZ`0&?JV`+M3=p?W?&9N_oWSP;7B}FJ0;=(ue1m@;BiCeW fE_n$q1{(%_h5&{lhJ1!BhE#@Rh7txl1}+8w9~4LM diff --git a/src/Api/TakeoutSaaS.UserApi/Program.cs b/src/Api/TakeoutSaaS.UserApi/Program.cs index 98ed208..5c93992 100644 --- a/src/Api/TakeoutSaaS.UserApi/Program.cs +++ b/src/Api/TakeoutSaaS.UserApi/Program.cs @@ -7,12 +7,15 @@ using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Serilog; using TakeoutSaaS.Module.Tenancy.Extensions; +using TakeoutSaaS.Shared.Abstractions.Ids; +using TakeoutSaaS.Shared.Kernel.Ids; using TakeoutSaaS.Shared.Web.Extensions; using TakeoutSaaS.Shared.Web.Swagger; var builder = WebApplication.CreateBuilder(args); const string logTemplate = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [TraceId:{TraceId}] [SpanId:{SpanId}] [Service:{Service}] {SourceContext} {Message:lj}{NewLine}{Exception}"; +builder.Services.AddSingleton(_ => new SnowflakeIdGenerator()); builder.Host.UseSerilog((_, _, configuration) => { configuration diff --git a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj index b5f3476cfdae878b68d3fd34e022ff5dafb02903..2a318505dc9dbe42de413defa0a61adf1ca5073f 100644 GIT binary patch delta 303 zcmca3@k(k!05dNG*F=ABM&5}tRe{9bct+mI(u|HkGMX_GNN#3q>(tRjXyAejRZ;YJdf%+DhZ($6U`0rD)+qYME+)%grrK=sKCB@A{9 E0Q7uLYXATM delta 184 zcmaDQbw^@Cz{C)*i3?OF9*di-#^^9PgE3@+?j{upJAy wd?u@KRZU*NbpwcfCLiOT0Fsm7Vz6P*X9!>@V#sI6Vn}63W+-8>W8h){0I=CUx&QzG From 5361451140dd33ca16e245d728be2d03b36085bd Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 13:53:49 +0800 Subject: [PATCH 4/5] =?UTF-8?q?ci:=20pull=20request=20=E6=89=8D=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci-cd.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 6e12b41..2c21c1d 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -1,10 +1,6 @@ name: TakeoutSaaS CI/CD on: - push: - branches: - - main - - dev pull_request: branches: - main From 6b5da04751f97de0d9491dfb1f52d725f4f2fba1 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Wed, 3 Dec 2025 14:49:33 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=BD=91?= =?UTF-8?q?=E5=85=B3=E5=AE=B9=E5=99=A8=E5=8C=96=E4=B8=8E=E9=99=90=E6=B5=81?= =?UTF-8?q?=E7=9B=91=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Document/05_部署运维.md | 47 +++++ .../GatewayOpenTelemetryOptions.cs | 22 +++ .../Configuration/GatewayRateLimitOptions.cs | 27 +++ src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile | 25 +++ src/Gateway/TakeoutSaaS.ApiGateway/Program.cs | 170 ++++++++++++++---- .../TakeoutSaaS.ApiGateway.csproj | Bin 718 -> 1922 bytes .../appsettings.Development.json | 3 + .../TakeoutSaaS.ApiGateway/appsettings.json | 75 ++++++++ 8 files changed, 332 insertions(+), 37 deletions(-) create mode 100644 src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs create mode 100644 src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayRateLimitOptions.cs create mode 100644 src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile create mode 100644 src/Gateway/TakeoutSaaS.ApiGateway/appsettings.json diff --git a/Document/05_部署运维.md b/Document/05_部署运维.md index 2d90a48..f92e303 100644 --- a/Document/05_部署运维.md +++ b/Document/05_部署运维.md @@ -1009,3 +1009,50 @@ docker-compose up -d --force-recreate --no-deps api docker pull takeout-saas-api:previous-version docker-compose up -d ``` + +## 13. 网关 TakeoutSaaS.ApiGateway 部署 + +1. **部署拓扑** + - Nginx 负责域名 `kjkj.qiyuesns.cn`(含后续 HTTPS 证书),并将所有流量反代到本机 `http://127.0.0.1:5000`。 + - .NET 网关容器(TakeoutSaaS.ApiGateway)负责 YARP 路由、限流、日志与 OpenTelemetry 埋点,向下游 49.7.179.246 的 Admin/User/Mini API 转发。 + +2. **构建与运行** + ```bash + # 构建镜像 + docker build -f src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile -t takeoutsaas/apigateway:latest . + + # 启动容器(生产环境建议挂载独立配置) + docker run -d --name takeout-gateway -p 5000:5000 ^ + -e ASPNETCORE_ENVIRONMENT=Production ^ + -v /opt/takeoutsaas/gateway/appsettings.Production.json:/app/appsettings.Production.json ^ + takeoutsaas/apigateway:latest + ``` + - `appsettings.json` 默认将 `/api/admin|mini|user/**` 指向主应用服务器(7801/7701/7901 端口)。 + - `appsettings.Development.json` 可在本地覆盖为 `localhost:5001/5002/5003`,无需改代码。 + +3. **Nginx 参考配置** + ```nginx + server { + listen 80; + server_name kjkj.qiyuesns.cn; + + location / { + proxy_pass http://127.0.0.1:5000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + ``` + - 后续启用 HTTPS 时,将 `listen 443 ssl` 与证书配置加入同一 `server`,其余保持不变。 + +4. **关键配置项说明** + - `Gateway:RateLimiting`:按客户端 IP 固定窗口限流(默认 300 次/60 秒),可通过配置文件调整或关闭。 + - `ReverseProxy`:集中声明路由规则(`/api/{service}/**`),后端地址变更时只需改配置即可。 + - `OpenTelemetry`:默认开启 OTLP 导出,Collector 地址通过 `OpenTelemetry:OtlpEndpoint` 指定。 + - `Serilog`:统一输出到控制台,日志采集器可以直接收集 Docker stdout。 + +5. **健康检查** + - `GET /healthz`:基础健康,用于探活或监控告警。 + - `GET /`:返回服务元信息,可作为简易诊断接口。 diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs new file mode 100644 index 0000000..97064fb --- /dev/null +++ b/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs @@ -0,0 +1,22 @@ +namespace TakeoutSaaS.ApiGateway.Configuration; + +/// +/// 网关 OpenTelemetry 导出配置。 +/// +public class GatewayOpenTelemetryOptions +{ + /// + /// 是否启用 OpenTelemetry。 + /// + public bool Enabled { get; set; } = true; + + /// + /// 服务名称,用于 Resource 标识。 + /// + public string ServiceName { get; set; } = "TakeoutSaaS.ApiGateway"; + + /// + /// OTLP 导出端点(http/https)。 + /// + public string? OtlpEndpoint { get; set; } +} diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayRateLimitOptions.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayRateLimitOptions.cs new file mode 100644 index 0000000..cf633a4 --- /dev/null +++ b/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayRateLimitOptions.cs @@ -0,0 +1,27 @@ +namespace TakeoutSaaS.ApiGateway.Configuration; + +/// +/// 网关限流参数配置。 +/// +public class GatewayRateLimitOptions +{ + /// + /// 是否开启固定窗口限流。 + /// + public bool Enabled { get; set; } = true; + + /// + /// 固定窗口内允许的最大请求数。 + /// + public int PermitLimit { get; set; } = 300; + + /// + /// 固定窗口长度(秒)。 + /// + public int WindowSeconds { get; set; } = 60; + + /// + /// 排队等待的最大请求数。 + /// + public int QueueLimit { get; set; } = 100; +} diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile b/src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile new file mode 100644 index 0000000..beac7d4 --- /dev/null +++ b/src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile @@ -0,0 +1,25 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# 1. 先只复制 csproj 文件 (利用 Docker 缓存) +COPY ["src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj", "src/Gateway/TakeoutSaaS.ApiGateway/"] + +# 2. 还原依赖 (如果 csproj 没变,这一步会直接使用缓存,瞬间完成) +RUN dotnet restore "src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj" + +# 3. 再复制剩余的所有源代码 +COPY . . + +# 4. 发布 +RUN dotnet publish "src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj" -c Release -o /app/publish + +# --- 运行时环境 --- +FROM mcr.microsoft.com/dotnet/aspnet:10.0 +WORKDIR /app +COPY --from=build /app/publish . + +# 显式声明端口 +EXPOSE 5000 +ENV ASPNETCORE_URLS=http://+:5000 + +ENTRYPOINT ["dotnet", "TakeoutSaaS.ApiGateway.dll"] \ No newline at end of file diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs index f7467d5..2e76835 100644 --- a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs +++ b/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs @@ -1,38 +1,129 @@ +using System.Diagnostics; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.RateLimiting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System.Diagnostics; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Serilog; +using TakeoutSaaS.ApiGateway.Configuration; using System.Threading.RateLimiting; +const string CorsPolicyName = "GatewayCors"; + var builder = WebApplication.CreateBuilder(args); +builder.Host.UseSerilog((context, services, loggerConfiguration) => +{ + loggerConfiguration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext(); +}); + builder.Services.AddReverseProxy() .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); -builder.Services.AddRateLimiter(options => +builder.Services.Configure(options => { - options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; - options.GlobalLimiter = PartitionedRateLimiter.Create(httpContext => + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.KnownIPNetworks.Clear(); + options.KnownProxies.Clear(); +}); + +builder.Services.AddCors(options => +{ + options.AddPolicy(CorsPolicyName, policy => { - const string partitionKey = "proxy-default"; - return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new FixedWindowRateLimiterOptions - { - PermitLimit = 100, - Window = TimeSpan.FromSeconds(1), - QueueLimit = 50, - QueueProcessingOrder = QueueProcessingOrder.OldestFirst - }); + policy.AllowAnyOrigin() + .AllowAnyHeader() + .AllowAnyMethod(); }); }); +builder.Services.Configure(builder.Configuration.GetSection("Gateway:RateLimiting")); +var rateLimitOptions = builder.Configuration.GetSection("Gateway:RateLimiting").Get() ?? new(); + +if (rateLimitOptions.Enabled) +{ + builder.Services.AddRateLimiter(options => + { + options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; + options.GlobalLimiter = PartitionedRateLimiter.Create(context => + { + var remoteIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; + return RateLimitPartition.GetFixedWindowLimiter(remoteIp, _ => new FixedWindowRateLimiterOptions + { + PermitLimit = Math.Max(1, rateLimitOptions.PermitLimit), + Window = TimeSpan.FromSeconds(Math.Max(1, rateLimitOptions.WindowSeconds)), + QueueLimit = Math.Max(0, rateLimitOptions.QueueLimit), + QueueProcessingOrder = QueueProcessingOrder.OldestFirst + }); + }); + }); +} + +var otelOptions = builder.Configuration.GetSection("OpenTelemetry").Get() ?? new(); +if (otelOptions.Enabled) +{ + builder.Services.AddOpenTelemetry() + // 1. 配置统一的 Resource,便于追踪定位。 + .ConfigureResource(resource => resource.AddService(otelOptions.ServiceName ?? "TakeoutSaaS.ApiGateway")) + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + + if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint)) + { + metrics.AddOtlpExporter(options => + { + options.Endpoint = new Uri(otelOptions.OtlpEndpoint); + }); + } + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation(options => + { + options.RecordException = true; + }) + .AddHttpClientInstrumentation(); + + if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint)) + { + tracing.AddOtlpExporter(options => + { + options.Endpoint = new Uri(otelOptions.OtlpEndpoint); + }); + } + }); + + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeScopes = true; + logging.ParseStateValues = true; + if (!string.IsNullOrWhiteSpace(otelOptions.OtlpEndpoint)) + { + logging.AddOtlpExporter(options => + { + options.Endpoint = new Uri(otelOptions.OtlpEndpoint); + }); + } + }); +} + var app = builder.Build(); +app.UseForwardedHeaders(); + app.UseExceptionHandler(errorApp => { + // 1. 捕获所有未处理异常并返回统一结构。 errorApp.Run(async context => { var feature = context.Features.Get(); @@ -54,36 +145,40 @@ app.UseExceptionHandler(errorApp => }); }); -app.Use(async (context, next) => +app.UseSerilogRequestLogging(options => { - var logger = context.RequestServices.GetRequiredService().CreateLogger("Gateway"); - var start = DateTime.UtcNow; - await next(context); - var elapsed = DateTime.UtcNow - start; - logger.LogInformation("Gateway {Method} {Path} => {Status} ({Elapsed} ms)", - context.Request.Method, - context.Request.Path, - context.Response.StatusCode, - (int)elapsed.TotalMilliseconds); + options.MessageTemplate = "网关请求 {RequestMethod} {RequestPath} => {StatusCode} 用时 {Elapsed:0.000} 秒"; + options.GetLevel = (httpContext, elapsed, ex) => ex is not null ? Serilog.Events.LogEventLevel.Error : Serilog.Events.LogEventLevel.Information; + options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => + { + diagnosticContext.Set("TraceId", Activity.Current?.Id ?? httpContext.TraceIdentifier); + diagnosticContext.Set("ClientIp", httpContext.Connection.RemoteIpAddress?.ToString()); + }; }); -app.UseRateLimiter(); +app.UseCors(CorsPolicyName); + +if (rateLimitOptions.Enabled) +{ + app.UseRateLimiter(); +} app.Use(async (context, next) => { - // 确保存在请求 ID,便于上下游链路追踪。 + // 1. 确保请求拥有可追踪的 ID。 if (!context.Request.Headers.ContainsKey("X-Request-Id")) { - context.Request.Headers["X-Request-Id"] = Guid.NewGuid().ToString("N"); + context.Request.Headers["X-Request-Id"] = Activity.Current?.Id ?? Guid.NewGuid().ToString("N"); } - // 透传租户与认证头。 + // 2. 透传租户等标识头,方便下游继续使用。 var tenantId = context.Request.Headers["X-Tenant-Id"]; - var tenantCode = context.Request.Headers["X-Tenant-Code"]; if (!string.IsNullOrWhiteSpace(tenantId)) { context.Request.Headers["X-Tenant-Id"] = tenantId; } + + var tenantCode = context.Request.Headers["X-Tenant-Code"]; if (!string.IsNullOrWhiteSpace(tenantCode)) { context.Request.Headers["X-Tenant-Code"] = tenantCode; @@ -92,13 +187,7 @@ app.Use(async (context, next) => await next(context); }); -app.MapReverseProxy(proxyPipeline => -{ - proxyPipeline.Use(async (context, next) => - { - await next().ConfigureAwait(false); - }); -}); +app.MapReverseProxy(); app.MapGet("/", () => Results.Json(new { @@ -107,4 +196,11 @@ app.MapGet("/", () => Results.Json(new Timestamp = DateTimeOffset.UtcNow })); +app.MapGet("/healthz", () => Results.Json(new +{ + Service = "TakeoutSaaS.ApiGateway", + Status = "Healthy", + Timestamp = DateTimeOffset.UtcNow +})); + app.Run(); diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj b/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj index 463d6c9dc237fd0090c02da96961fc2236bfc252..8d989af8a36f0f03f7887b55044b5915d1937cc2 100644 GIT binary patch literal 1922 zcmdUw%Wl(P5QWbgsqbL1$7#4!U6@n|1=0njN>WgliR)A$!Le)yl9zA$&A1Mb)xeb7uZCXD+_JigcuvzG|*O-;^jj<4|v9IEkI1LhB{GKL;fpNsL%L~iPh1bp5Ps9B4*C5 z)LQR7#!}f&s0umM2-_vdQt!AMkyoPcoR-6(18=jm$;h&b)Z*;j9yN<9{)v|8G{MKJ&Svy?MBy;?c7TB-5NzY=EMXB%?h59O^j9} zSFsOz!Y}tcW3B-)tPkcWaly0o$VW0V9fn*@71O-LXN}5TdMk7N?c|&{&FCtJ614T% z+`I0Rdu?}QTQ9wW)|ah2UGNY&82fZH83m{J;8>7nH4Vn(F>tl&7^|H6SRGRCN6ykF zI@BAi8C_}>dh2yb9*xewpnFw{ij}uf602_Q!{0T7R>RVRQ=HBDr83* z-@h|k+iV6tGc!6d^d7X1wRvyq#8(f%>71#^>7`J%