diff --git a/.gitmodules b/.gitmodules index c0e417a..158e990 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,7 @@ [submodule "TakeoutSaaS.Docs"] path = TakeoutSaaS.Docs url = git@github.com:msumshk/TakeoutSaaS.Docs.git +[submodule "src/Gateway/TakeoutSaaS.ApiGateway"] + path = src/Gateway/TakeoutSaaS.ApiGateway + url = git@github.com:msumshk/TakeoutSaaS.Gateway.git + branch = dev diff --git a/src/Gateway/TakeoutSaaS.ApiGateway b/src/Gateway/TakeoutSaaS.ApiGateway new file mode 160000 index 0000000..2b64020 --- /dev/null +++ b/src/Gateway/TakeoutSaaS.ApiGateway @@ -0,0 +1 @@ +Subproject commit 2b6402058dfddc3aed6d5ff8052dcd68cf4ee632 diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs b/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs deleted file mode 100644 index 97064fb..0000000 --- a/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayOpenTelemetryOptions.cs +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index cf633a4..0000000 --- a/src/Gateway/TakeoutSaaS.ApiGateway/Configuration/GatewayRateLimitOptions.cs +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index beac7d4..0000000 --- a/src/Gateway/TakeoutSaaS.ApiGateway/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 6011bd9..0000000 --- a/src/Gateway/TakeoutSaaS.ApiGateway/Program.cs +++ /dev/null @@ -1,215 +0,0 @@ -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.HttpOverrides; -using OpenTelemetry.Logs; -using OpenTelemetry.Metrics; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using Serilog; -using System.Diagnostics; -using System.Threading.RateLimiting; -using TakeoutSaaS.ApiGateway.Configuration; - -const string CorsPolicyName = "GatewayCors"; - -// 1. 创建构建器并配置 Serilog -var builder = WebApplication.CreateBuilder(args); -builder.Host.UseSerilog((context, services, loggerConfiguration) => -{ - loggerConfiguration - .ReadFrom.Configuration(context.Configuration) - .ReadFrom.Services(services) - .Enrich.FromLogContext(); -}); - -// 2. 配置 YARP 反向代理 -builder.Services.AddReverseProxy() - .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy")); - -// 3. 转发头部配置 -builder.Services.Configure(options => -{ - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - options.KnownIPNetworks.Clear(); - options.KnownProxies.Clear(); -}); - -// 4. 配置 CORS -builder.Services.AddCors(options => -{ - options.AddPolicy(CorsPolicyName, policy => - { - policy.AllowAnyOrigin() - .AllowAnyHeader() - .AllowAnyMethod(); - }); -}); - -// 5. 配置网关限流 -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 - }); - }); - }); -} - -// 6. 配置 OpenTelemetry -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); - }); - } - }); -} - -// 7. 构建应用 -var app = builder.Build(); - -// 8. 转发头中间件 -app.UseForwardedHeaders(); - -// 9. 全局异常处理中间件 -app.UseExceptionHandler(errorApp => -{ - // 1. 捕获所有未处理异常并返回统一结构。 - errorApp.Run(async context => - { - var feature = context.Features.Get(); - var traceId = Activity.Current?.Id ?? context.TraceIdentifier; - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - context.Response.ContentType = "application/json"; - - var payload = new - { - success = false, - code = 500, - message = "Gateway internal error", - traceId - }; - - var logger = context.RequestServices.GetRequiredService().CreateLogger("Gateway"); - logger.LogError(feature?.Error, "网关异常 {TraceId}", traceId); - await context.Response.WriteAsJsonAsync(payload, cancellationToken: context.RequestAborted); - }); -}); - -// 10. 请求日志 -app.UseSerilogRequestLogging(options => -{ - 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()); - }; -}); - -// 11. CORS 与限流 -app.UseCors(CorsPolicyName); - -if (rateLimitOptions.Enabled) -{ - app.UseRateLimiter(); -} - -// 12. 透传请求头并保证 Trace -app.Use(async (context, next) => -{ - // 1. 确保请求拥有可追踪的 ID。 - if (!context.Request.Headers.ContainsKey("X-Request-Id")) - { - context.Request.Headers["X-Request-Id"] = Activity.Current?.Id ?? Guid.NewGuid().ToString("N"); - } - - // 2. 透传租户等标识头,方便下游继续使用。 - var tenantId = context.Request.Headers["X-Tenant-Id"]; - 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; - } - - await next(context); -}); - -// 13. 映射反向代理与健康接口 -app.MapReverseProxy(); - -app.MapGet("/", () => Results.Json(new -{ - Service = "TakeoutSaaS.ApiGateway", - Status = "OK", - 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/Properties/launchSettings.json b/src/Gateway/TakeoutSaaS.ApiGateway/Properties/launchSettings.json deleted file mode 100644 index 3956499..0000000 --- a/src/Gateway/TakeoutSaaS.ApiGateway/Properties/launchSettings.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "profiles": { - "TakeoutSaaS.ApiGateway": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:2677;http://localhost:2683" - } - } -} \ No newline at end of file diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj b/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj deleted file mode 100644 index 207e703..0000000 Binary files a/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj and /dev/null differ diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json b/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json deleted file mode 100644 index 56dd41a..0000000 --- a/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.Development.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "OpenTelemetry": { - "Enabled": false - }, - "ReverseProxy": { - "Routes": { - "admin-route": { - "ClusterId": "admin", - "Match": { "Path": "/api/admin/{**catch-all}" } - }, - "mini-route": { - "ClusterId": "mini", - "Match": { "Path": "/api/mini/{**catch-all}" } - }, - "user-route": { - "ClusterId": "user", - "Match": { "Path": "/api/user/{**catch-all}" } - } - }, - "Clusters": { - "admin": { - "Destinations": { - "d1": { "Address": "http://localhost:5001/" } - } - }, - "mini": { - "Destinations": { - "d1": { "Address": "http://localhost:5002/" } - } - }, - "user": { - "Destinations": { - "d1": { "Address": "http://localhost:5003/" } - } - } - } - } -} diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.json b/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.json deleted file mode 100644 index 0c5d718..0000000 --- a/src/Gateway/TakeoutSaaS.ApiGateway/appsettings.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "Serilog": { - "Using": [ "Serilog.Sinks.Console" ], - "MinimumLevel": { - "Default": "Information", - "Override": { - "Microsoft": "Warning", - "System": "Warning", - "Yarp": "Information" - } - }, - "WriteTo": [ - { - "Name": "Console", - "Args": { - "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}" - } - } - ], - "Enrich": [ "FromLogContext" ], - "Properties": { - "Application": "TakeoutSaaS.ApiGateway" - } - }, - "Gateway": { - "RateLimiting": { - "Enabled": true, - "PermitLimit": 300, - "WindowSeconds": 60, - "QueueLimit": 100 - } - }, - "OpenTelemetry": { - "Enabled": true, - "ServiceName": "TakeoutSaaS.ApiGateway", - "OtlpEndpoint": "http://localhost:4317" - }, - "ReverseProxy": { - "Routes": { - "admin-route": { - "ClusterId": "admin", - "Match": { "Path": "/api/admin/{**catch-all}" } - }, - "mini-route": { - "ClusterId": "mini", - "Match": { "Path": "/api/mini/{**catch-all}" } - }, - "user-route": { - "ClusterId": "user", - "Match": { "Path": "/api/user/{**catch-all}" } - } - }, - "Clusters": { - "admin": { - "Destinations": { - "primary": { "Address": "http://49.7.179.246:7801/" } - } - }, - "mini": { - "Destinations": { - "primary": { "Address": "http://49.7.179.246:7701/" } - } - }, - "user": { - "Destinations": { - "primary": { "Address": "http://49.7.179.246:7901/" } - } - } - } - }, - "AllowedHosts": "*" -}